diff --git a/website/community/how-to-release/creating-a-fluss-release.mdx b/website/community/how-to-release/creating-a-fluss-release.mdx index 727d86b02f..e66e3a46b5 100644 --- a/website/community/how-to-release/creating-a-fluss-release.mdx +++ b/website/community/how-to-release/creating-a-fluss-release.mdx @@ -481,7 +481,7 @@ $ cd docker/fluss docker/fluss $ docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag apache/fluss:${RELEASE_VERSION}-rc${RC_NUM} . ``` -Then, run the following commands to build and push the flink — a customized Apache Flink image that includes all necessary libraries and JARs for the Fluss Quickstart guide: +Then, run the following commands to build and push the flink, a customized Apache Flink image that includes all necessary libraries and JARs for the Fluss Quickstart guide: ```bash docker/fluss $ cd ../quickstart-flink diff --git a/website/community/how-to-release/verifying-a-fluss-release.md b/website/community/how-to-release/verifying-a-fluss-release.md index b75d4c728b..a1649c17c7 100644 --- a/website/community/how-to-release/verifying-a-fluss-release.md +++ b/website/community/how-to-release/verifying-a-fluss-release.md @@ -94,7 +94,7 @@ And then you can use the staged maven artifacts as dependencies in the project a For any user-facing feature included in a release, we aim to ensure it is functional, usable, and well-documented for the Fluss community. -To support this, release managers can create and assign cross-team testing issues that outline key scenarios to validate. These issues are open to—and encouraged for—all community members to pick up and help verify. +To support this, release managers can create and assign cross-team testing issues that outline key scenarios to validate. These issues are open to, and encouraged for, all community members to pick up and help verify. A great way to get started is by walking through the official Quickstart Guide: https://fluss.apache.org/docs/quickstart/flink/ (please switch to the documentation version currently under release). diff --git a/website/docs/assets/architecture.png b/website/docs/assets/architecture.png index cd80e85023..431d65a2a7 100644 Binary files a/website/docs/assets/architecture.png and b/website/docs/assets/architecture.png differ diff --git a/website/docs/assets/data_organization.png b/website/docs/assets/data_organization.png index ce5281048f..02ceead7f9 100644 Binary files a/website/docs/assets/data_organization.png and b/website/docs/assets/data_organization.png differ diff --git a/website/docs/assets/delta_join.jpg b/website/docs/assets/delta_join.jpg deleted file mode 100644 index baa460880f..0000000000 Binary files a/website/docs/assets/delta_join.jpg and /dev/null differ diff --git a/website/docs/assets/delta_join.png b/website/docs/assets/delta_join.png new file mode 100644 index 0000000000..b1f34dfb5a Binary files /dev/null and b/website/docs/assets/delta_join.png differ diff --git a/website/docs/assets/deployment_overview.png b/website/docs/assets/deployment_overview.png index 380e34cf21..76e3a7fab9 100644 Binary files a/website/docs/assets/deployment_overview.png and b/website/docs/assets/deployment_overview.png differ diff --git a/website/docs/assets/streamhouse.png b/website/docs/assets/streamhouse.png index 36e342f6e2..889a07c489 100644 Binary files a/website/docs/assets/streamhouse.png and b/website/docs/assets/streamhouse.png differ diff --git a/website/docs/assets/tiered-storage.png b/website/docs/assets/tiered-storage.png index 2d9f437495..a8bd0c20a9 100644 Binary files a/website/docs/assets/tiered-storage.png and b/website/docs/assets/tiered-storage.png differ diff --git a/website/docs/engine-flink/delta-joins.md b/website/docs/engine-flink/delta-joins.md index 44f6f3746d..23690de45f 100644 --- a/website/docs/engine-flink/delta-joins.md +++ b/website/docs/engine-flink/delta-joins.md @@ -19,7 +19,7 @@ Starting with **Apache Fluss 0.8**, streaming join jobs running on **Flink 2.1 o Traditional streaming joins in Flink require maintaining both input sides entirely in state to match records across streams. Delta join, by contrast, uses a **index-key lookup mechanism** to transform the behavior of querying data from the state into querying data from the Fluss source table, thereby avoiding redundant storage of the same data in both the Fluss source table and the state. This drastically reduces state size and improves performance for many streaming analytics and enrichment workloads. -![](../assets/delta_join.jpg) +![](../assets/delta_join.png) ## Example: Delta Join in Flink 2.1 diff --git a/website/docs/intro.md b/website/docs/intro.md deleted file mode 100644 index c2d0858632..0000000000 --- a/website/docs/intro.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Introduction -sidebar_position: 1 -slug: / ---- - -# What is Fluss? - -Fluss is a streaming storage built for real-time analytics & AI which can serve as the real-time data layer for Lakehouse architectures. - -![arch](/img/fluss.png) - -It bridges the gap between **streaming data** and the data **Lakehouse** by enabling low-latency, high-throughput data ingestion and processing while seamlessly integrating with popular compute engines like **Apache Flink**, with **Apache Spark** and **StarRocks** coming soon. - -Fluss supports `streaming reads` and `writes` with sub-second latency and stores data in a columnar format, enhancing query performance and reducing storage costs. -It offers flexible table types, including append-only **Log Tables** and updatable **PrimaryKey Tables**, to accommodate diverse real-time analytics and processing needs. - -With built-in replication for fault tolerance, horizontal scalability, and advanced features like high-QPS lookup joins and bulk read/write operations, Fluss is ideal for powering **real-time analytics**, **AI/ML pipelines**, and **streaming data warehouses**. - -**Fluss (German: river, pronounced `/flus/`)** enables streaming data continuously converging, distributing and flowing into lakes, like a river 🌊 - -## Use Cases -The following is a list of (but not limited to) use-cases that Fluss shines ✨: -* **📊 Optimized Real-time analytics** -* **🔧 Feature Stores** -* **📈 Real-time Dashboards** -* **🧍 Real-time Customer 360** -* **📡 Real-time IoT Pipelines** -* **🚓 Real-time Fraud Detection** -* **🚨 Real-time Alerting Systems** -* **💫 Real-time ETL/Data Warehouses** -* **🌐 Real-time Geolocation Services** -* **🚚 Real-time Shipment Update Tracking** - -## Where to go Next? - -- [QuickStart](quickstart/flink.md): Get started with Fluss in minutes. -- [Architecture](concepts/architecture.md): Learn about Fluss's architecture. -- [Table Design](table-design/overview.md): Explore Fluss's table types, partitions and buckets. -- [Lakehouse](streaming-lakehouse/overview.md): Integrate Fluss with your Lakehouse to bring low-latency data to your Lakehouse analytics. -- [Development](/community/dev/ide-setup): Set up your development environment and contribute to the community. diff --git a/website/docs/intro.mdx b/website/docs/intro.mdx new file mode 100644 index 0000000000..dac23a6c66 --- /dev/null +++ b/website/docs/intro.mdx @@ -0,0 +1,60 @@ +--- +title: Introduction +sidebar_position: 1 +slug: / +--- + +# What is Apache Fluss? + +**One platform. Streams, tables, and the lakehouse.** Apache Fluss is an open-source streaming storage system for real-time analytics and AI, designed to serve as the real-time data layer of a Lakehouse architecture. + +Fluss collapses the message broker, the online key-value store, the stream-processing state backend, and the lakehouse cold store into a single coherent foundation, so the same table can be read at sub-second freshness for analytics, looked up by primary key for features, and tiered into open formats like Apache Iceberg, Apache Paimon, or Lance for historical scans. + +![arch](/img/fluss.png) + +It integrates first-class with **Apache Flink** and **Apache Spark**, supports streaming reads and writes with sub-second latency, stores data in a columnar layout for projection and predicate pushdown, and offers two table types (append-only **Log Tables** and updatable **PrimaryKey Tables**) to cover both event-stream and database-style workloads. + +**Fluss (German: river, pronounced `/flus/`)** carries streaming data continuously into lakes, like a river 🌊 + +## Use cases + +Fluss targets workloads where the same data must be read at multiple freshness layers (stream, point-lookup, and lake) without being copied across systems. Six use cases drive most deployments today. + +### 1. Real-time feature stores for ML serving and training + +The single largest use case. A Fluss **PrimaryKey Table** holds the live feature row in RocksDB on the leader for sub-millisecond key-value lookup at serving time, and the ordered changelog for training reads. Both views read the same underlying storage, so training/serving skew is structurally eliminated rather than monitored after the fact. Replaces the typical Redis-online-store plus Iceberg-offline-store pair with a single substrate. + +### 2. Real-time risk, fraud detection, and decision audit + +PrimaryKey Tables emit a complete `+I / -U / +U / -D` changelog for every entity. When a real-time decision fires (decline a transaction, flag an account, deny a request), the exact feature values that drove it trace directly back to a specific changelog record, so the decision is fully reconstructible from the log itself, with no secondary audit pipeline. + +### 3. Real-time entity profiles and Customer 360 + +The **Aggregation Merge Engine** combined with **Roaring Bitmaps** lets the leader maintain large entity sets such as *"users who clicked in the last ten minutes"* or *"accounts that crossed a usage threshold today"*, via at-write-time read-modify-write. Profile composition reduces to set algebra (`AND` / `OR` / `AND NOT`) over bitmap columns. Multiple producers can write disjoint columns of the same wide row independently via partial updates. + +### 4. AI agent memory and context engineering for LLM systems + +Four distinct memory primitives live in the same substrate under a consistent schema surface: + +- **Session memory**: conversation logs +- **Entity memory**: live key-value facts +- **Behavioral memory**: streaming features +- **Semantic memory**: vector store via Lance + +An agent reads all four concurrently to assemble a grounded prompt, with no cross-system synchronization. + +### 5. Real-time operational analytics and OLAP hot-store replacement + +The columnar Arrow log plus server-side compound pruning (partition pruning, predicate pushdown, column projection) lets analytical engines such as Flink batch, Spark, Trino, StarRocks, and DuckDB query the hot tier directly. The result is order-of-magnitude reductions in I/O, network, and deserialization cost. Replaces the typical "OLAP hot store" sitting alongside Kafka. + +### 6. Streaming ETL with externalized state + +Stateful streaming ETL pipelines (joins, rolling aggregations, deduplication, reference-data enrichment, wide-row assembly across multiple producers) traditionally accumulate gigabytes of operator state inside Flink that must be checkpointed, recovered, and rebalanced on scale-out. With Fluss, that state moves into **PrimaryKey Tables** on the leader and the Flink job becomes stateless: dual-stream joins collapse into stateless index-key lookups via **delta joins**, rolling counts and velocity signals collapse into writes against the **Aggregation Merge Engine**, and multi-producer wide-row updates collapse into **partial-updates** against a shared row. + +## Where to go next? + +- [QuickStart](quickstart/flink.md): Get started with Fluss in minutes. +- [Architecture](concepts/architecture.md): Learn about Fluss's architecture. +- [Table Design](table-design/overview.md): Explore Fluss's table types, partitions and buckets. +- [Lakehouse](streaming-lakehouse/overview.md): Integrate Fluss with your Lakehouse to bring low-latency data to your Lakehouse analytics. +- [Development](/community/dev/ide-setup): Set up your development environment and contribute to the community. diff --git a/website/docs/maintenance/tiered-storage/overview.md b/website/docs/maintenance/tiered-storage/overview.md index 898af077d5..6d8a5481b0 100644 --- a/website/docs/maintenance/tiered-storage/overview.md +++ b/website/docs/maintenance/tiered-storage/overview.md @@ -18,4 +18,4 @@ in the well-known open data lake format for better analytics performance. Curren The overall tiered storage architecture is shown in the following diagram: - + diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index a7853f1ef7..e8802d349e 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -15,19 +15,81 @@ * limitations under the License. */ -import {themes as prismThemes} from 'prism-react-renderer'; import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; import lightTheme from './src/utils/prismLight'; +import darkTheme from './src/utils/prismDark'; import versionReplace from './src/plugins/remark-version-replace/index'; import { loadVersionData } from './src/utils/versionData'; const { versionsMap, latestVersion } = loadVersionData(); const config: Config = { title: 'Apache Fluss™ (Incubating)', - tagline: 'Streaming Storage for Real-Time Analytics & AI', + tagline: 'The streaming storage layer for real-time analytics and the lakehouse', favicon: 'img/logo/fluss_favicon.svg', + headTags: [ + { + tagName: 'meta', + attributes: { + name: 'description', + content: + 'Apache Fluss is an open-source columnar streaming storage system. Sub-second freshness, primary-key tables, first-class Apache Flink integration, and native tiering to Apache Iceberg and Apache Paimon.', + }, + }, + { + tagName: 'meta', + attributes: { + property: 'og:title', + content: 'Apache Fluss · Streaming Storage for the Real-Time Lakehouse', + }, + }, + { + tagName: 'meta', + attributes: { + property: 'og:description', + content: + 'Open-source columnar streaming storage with sub-second freshness, primary-key tables, Flink integration, and native tiering to Iceberg and Paimon.', + }, + }, + { + tagName: 'meta', + attributes: { + property: 'og:type', + content: 'website', + }, + }, + { + tagName: 'meta', + attributes: { + name: 'twitter:card', + content: 'summary_large_image', + }, + }, + { + tagName: 'meta', + attributes: { + name: 'twitter:title', + content: 'Apache Fluss · Streaming Storage for the Real-Time Lakehouse', + }, + }, + { + tagName: 'meta', + attributes: { + name: 'twitter:description', + content: + 'Open-source columnar streaming storage with sub-second freshness, primary-key tables, Flink integration, and native tiering to Iceberg and Paimon.', + }, + }, + { + tagName: 'meta', + attributes: { + name: 'theme-color', + content: '#102856', + }, + }, + ], + // Set the production url of your site here url: 'https://fluss.apache.org/', // Set the // pathname under which your site is served @@ -67,8 +129,6 @@ const config: Config = { { docs: { sidebarPath: './sidebars.ts', - editUrl: ({docPath}) => - `https://github.com/apache/fluss/edit/main/website/docs/${docPath}`, remarkPlugins: [versionReplace], lastVersion: latestVersion, versions: versionsMap @@ -99,10 +159,9 @@ const config: Config = { path: 'community', routeBasePath: 'community', sidebarPath: './sidebarsCommunity.js', - editUrl: ({docPath}) => { - return `https://github.com/apache/fluss/edit/main/website/community/${docPath}`; - }, - // ... other options + // editUrl intentionally omitted so the "Edit this page" link does + // not appear at the bottom of community pages (mirrors the docs and + // blog presets, which also leave editUrl unset). }, ], [ @@ -116,16 +175,16 @@ const config: Config = { [ '@docusaurus/plugin-pwa', { - debug: true, + debug: false, offlineModeActivationStrategies: [ 'appInstalled', 'standalone', 'queryString', ], pwaHead: [ - { tagName: 'link', rel: 'icon', href: '/img/logo.svg' }, + { tagName: 'link', rel: 'icon', href: '/img/logo/svg/colored_logo.svg' }, { tagName: 'link', rel: 'manifest', href: '/manifest.json' }, - { tagName: 'meta', name: 'theme-color', content: '#0071e3' }, + { tagName: 'meta', name: 'theme-color', content: '#102856' }, ], }, ], @@ -160,13 +219,15 @@ const config: Config = { image: 'img/logo/png/colored_logo.png', colorMode: { defaultMode: 'light', - disableSwitch: true, + disableSwitch: false, + respectPrefersColorScheme: false, }, navbar: { title: '', logo: { alt: 'Fluss', - src: 'img/logo/svg/colored_logo.svg', + src: 'img/logo/svg/white_color_logo.svg', + srcDark: 'img/logo/svg/white_color_logo.svg', }, items: [ { @@ -194,17 +255,6 @@ const config: Config = { {to: '/community/welcome', label: 'Community', position: 'left'}, {to: '/roadmap', label: 'Roadmap', position: 'left'}, {to: '/downloads', label: 'Downloads', position: 'left'}, - { - label: 'ASF', position: 'right', items: [ - {to: 'https://www.apache.org/', label: 'Foundation'}, - {to: 'https://www.apache.org/licenses/', label: 'License'}, - {to: 'https://events.apache.org', label: 'Events'}, - {to: 'https://www.apache.org/foundation/sponsorship.html', label: 'Donate'}, - {to: 'https://www.apache.org/foundation/thanks.html', label: 'Sponsors'}, - {to: 'https://www.apache.org/security/', label: 'Security'}, - {to: 'https://privacy.apache.org/policies/privacy-policy-public.html', label: 'Privacy'} - ] - }, { type: 'docsVersionDropdown', position: 'right', @@ -220,6 +270,48 @@ const config: Config = { }, footer: { style: 'dark', + links: [ + { + title: 'Product', + items: [ + {label: 'Documentation', to: '/docs/quickstart/flink'}, + {label: 'Quickstart', to: '/docs/quickstart/flink'}, + {label: 'Roadmap', to: '/roadmap'}, + {label: 'Downloads', to: '/downloads'}, + {label: 'Blog', to: '/blog'}, + ], + }, + { + title: 'Community', + items: [ + {label: 'GitHub', href: 'https://github.com/apache/fluss'}, + {label: 'Slack', href: 'https://join.slack.com/t/apache-fluss/shared_invite/zt-33wlna581-QAooAiCmnYboJS8D_JUcYw'}, + {label: 'Welcome', to: '/community/welcome'}, + {label: 'Contribute', to: '/community/welcome'}, + ], + }, + { + title: 'Resources', + items: [ + {label: 'Talks', to: '/learn/talks'}, + {label: 'Videos', to: '/learn/videos'}, + {label: 'Issues', href: 'https://github.com/apache/fluss/issues'}, + {label: 'Releases', href: 'https://github.com/apache/fluss/releases'}, + ], + }, + { + title: 'Apache', + items: [ + {label: 'Foundation', href: 'https://www.apache.org/'}, + {label: 'License', href: 'https://www.apache.org/licenses/'}, + {label: 'Events', href: 'https://events.apache.org'}, + {label: 'Donate', href: 'https://www.apache.org/foundation/sponsorship.html'}, + {label: 'Sponsors', href: 'https://www.apache.org/foundation/thanks.html'}, + {label: 'Security', href: 'https://www.apache.org/security/'}, + {label: 'Privacy', href: 'https://privacy.apache.org/policies/privacy-policy-public.html'}, + ], + }, + ], logo: { width: 200, src: "/img/apache-incubator.svg", @@ -232,7 +324,7 @@ const config: Config = { }, prism: { theme: lightTheme, - darkTheme: prismThemes.dracula, + darkTheme: darkTheme, additionalLanguages: ['java', 'bash', 'scala'] }, algolia: { diff --git a/website/learn/talks.md b/website/learn/talks.md index 656e40126f..7fbaa0778c 100644 --- a/website/learn/talks.md +++ b/website/learn/talks.md @@ -20,7 +20,7 @@ This seminar session explores Fluss as the foundation of a Streaming Lakehouse m ### The Seven Deadly Sins of Streaming **Giannis Polyzos** • Big Data Conference Europe 2025 • December 2025 -Exploring the Streaming Lakehouse model—powered by Fluss’s columnar streaming storage—addresses the “Seven Deadly Sins of Streaming,” from redundant data copies and unqueryable streams to stale lakehouse data and costly architectures. By unifying streaming and lakehouse systems through streaming tables, Fluss enables real-time dashboards, streaming ETL, and Customer 360 use cases within a single, modern architecture that delivers fresher, more efficient real-time analytics. +Exploring the Streaming Lakehouse model, powered by Fluss’s columnar streaming storage, addresses the “Seven Deadly Sins of Streaming,” from redundant data copies and unqueryable streams to stale lakehouse data and costly architectures. By unifying streaming and lakehouse systems through streaming tables, Fluss enables real-time dashboards, streaming ETL, and Customer 360 use cases within a single, modern architecture that delivers fresher, more efficient real-time analytics. [📹 Watch on YouTube](https://www.youtube.com/watch?v=ZOh9XH4zGLM) @@ -38,7 +38,7 @@ This session explores how real-time data processing extends far beyond ingestion ### Fluss: Making Your Lakehouse Truly Real Time **Jark Wu** • Flink Forward 2025 • November 2025 -Exploring how Fluss bridges data streaming and the Lakehouse (Iceberg) by serving real-time data directly on top of it, enabling powerful analytics on streams while delivering low-latency updates to Iceberg—effectively transforming it into a Real-Time Lakehouse. We’ll close with real-world use cases that showcase how Fluss powers Real-Time Lakehouses and fuels the next generation of AI-driven applications. +Exploring how Fluss bridges data streaming and the Lakehouse (Iceberg) by serving real-time data directly on top of it, enabling powerful analytics on streams while delivering low-latency updates to Iceberg, effectively transforming it into a Real-Time Lakehouse. We’ll close with real-world use cases that showcase how Fluss powers Real-Time Lakehouses and fuels the next generation of AI-driven applications. [📹 Watch on YouTube](https://www.youtube.com/watch?v=pnrW5r-4mIQ) @@ -47,7 +47,7 @@ Exploring how Fluss bridges data streaming and the Lakehouse (Iceberg) by servin ### Apache Fluss and The Seven Deadly Sins of Streaming **Giannis Polyzos** • Flink Forward 2025 • November 2025 -Exploring how Apache Fluss addresses the “seven deadly sins” of streaming by introducing streams-as-tables that unify streaming and lakehouse systems, unlocking modern real-time analytics use cases such as real-time dashboards, streaming ETL, and Customer 360—all within a single, cohesive architecture. +Exploring how Apache Fluss addresses the “seven deadly sins” of streaming by introducing streams-as-tables that unify streaming and lakehouse systems, unlocking modern real-time analytics use cases such as real-time dashboards, streaming ETL, and Customer 360, all within a single, cohesive architecture. [📹 Watch on YouTube](https://www.youtube.com/watch?v=3c5RgJFTsMM) diff --git a/website/package.json b/website/package.json index 688385e72f..3256a63c6e 100644 --- a/website/package.json +++ b/website/package.json @@ -20,8 +20,8 @@ "@docusaurus/plugin-client-redirects": "^3.9.2", "@docusaurus/plugin-pwa": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", - "@fontsource/inter": "^5.2.8", - "@fontsource/roboto": "^5.2.10", + "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/geist-mono": "^5.2.7", "@mdx-js/react": "^3.0.0", "algoliasearch": "^5.10.2", "clsx": "^2.0.0", @@ -35,6 +35,8 @@ "@docusaurus/module-type-aliases": "^3.9.2", "@docusaurus/tsconfig": "^3.9.2", "@docusaurus/types": "^3.9.2", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", "typescript": "~5.5.2" }, "browserslist": { diff --git a/website/sidebars.ts b/website/sidebars.ts index 970dca8011..f253ab6a03 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -28,21 +28,25 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; Create as many sidebars as you want. */ const sidebars: SidebarsConfig = { - // By default, Docusaurus generates a sidebar from the docs folder structure - docsSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - /* - tutorialSidebar: [ - 'intro', - 'hello', + // By default, Docusaurus generates the docs sidebar from the docs folder + // structure. The "Compare" entry below points to standalone pages under + // /src/pages/compare/* (currently /compare/kafka), so readers can reach + // the comparison from any docs page, not only from the homepage. + docsSidebar: [ + {type: 'autogenerated', dirName: '.'}, { type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], + label: 'Compare', + collapsed: true, + items: [ + { + type: 'link', + label: 'Apache Fluss vs Apache Kafka', + href: '/compare/kafka', + }, + ], }, ], - */ }; export default sidebars; diff --git a/website/src/components/HomepageFeatures/index.tsx b/website/src/components/HomepageFeatures/index.tsx index 09671c2ce9..fa64c0f59a 100644 --- a/website/src/components/HomepageFeatures/index.tsx +++ b/website/src/components/HomepageFeatures/index.tsx @@ -20,79 +20,111 @@ import React from 'react'; import styles from './styles.module.css'; import Heading from '@theme/Heading'; - -type FeatureItem = { - title: string; - content: string; - Svg: React.ComponentType>; +type Pillar = { + number: string; + title: string; + summary: string; + body: string; + basis: string; + Svg: React.ComponentType>; }; -const FeatureList: FeatureItem[] = [ +/* Body copy was previously 35 to 50 words per card, which made the section + hard to scan (Jark feedback, PR #3226). Trimmed to ~22 words each so + the six pillars can be grasped in a single visual pass. */ +const PILLARS: Pillar[] = [ { - title: 'Sub-Second Data Freshness', - content: - 'Continuous ingestion and immediate availability of data enable low-latency analytics and real-time decision-making at scale.', - Svg: require('@site/static/img/feature_real_time.svg').default + number: '01', + title: 'Unified Architecture', + summary: 'One system for messaging, applications, analytics, and AI.', + body: 'Replaces the message queue, key-value store, and OLAP engine with a single platform serving transport, lookups, and queries from the same data.', + basis: 'Dual representation of PK Tables (Log Store & KV Store).', + Svg: require('@site/static/img/feature_update.svg').default, }, { - title: 'Streaming & Lakehouse Unification', - content: - 'Streaming-native storage with low-latency access on top of the lakehouse, using tables as a single abstraction to unify real-time and historical data across engines.', - Svg: require('@site/static/img/feature_lake.svg').default + number: '02', + title: 'Stream & Lakehouse Unification', + summary: 'One copy of data across real-time and batch layers.', + body: 'Hot and cold tiers share the same schema and are queryable as one substrate, so streaming and historical reads hit one source of truth.', + basis: 'Tiering Service and Union Read across Iceberg, Paimon, and Lance.', + Svg: require('@site/static/img/feature_lake.svg').default, }, { - title: 'Columnar Streaming', - content: - 'Based on Apache Arrow it allows database primitives on data streams and techniques like column pruning and predicate pushdown. This ensures engines read only the data they need, minimizing I/O and network costs.', - Svg: require('@site/static/img/feature_column.svg').default + number: '03', + title: 'Compute / Storage Separation', + summary: 'Lean, elastic, stateless compute with fast recovery.', + body: 'Stateless compute recovers in seconds and runs up to 85% cheaper than Kafka-based topologies. State lives on the Fluss leader, not Flink slots.', + basis: 'Stateless compute model with leader-resident state and KV snapshots.', + Svg: require('@site/static/img/feature_real_time.svg').default, }, { - title: 'Compute–Storage Separation', - content: - 'Stream processors focus on pure computation while Fluss manages state and storage, with features like deduplication, partial updates, delta joins, and aggregation merge engines.', - Svg: require('@site/static/img/feature_update.svg').default + number: '04', + title: 'Columnar Streaming Analytics', + summary: 'Pruning that compounds.', + body: 'Server-side projection, predicate pushdown, and partition pruning on Arrow-format streams compound into order-of-magnitude I/O and network savings.', + basis: 'ARROW log format and the compound pruning stack on the TabletServer.', + Svg: require('@site/static/img/feature_column.svg').default, }, { - title: 'ML & AI–Ready Storage', - content: - 'A unified storage layer supporting row-based, columnar, vector, and multi-modal data, enabling real-time feature stores and a centralized data repository for ML and AI systems.', - Svg: require('@site/static/img/feature_query.svg').default + number: '05', + title: 'Feature & Context Stores', + summary: 'Multi-modal data on one substrate, ready for ML and AI.', + body: 'Row, columnar, and vector data on one store. Online features, RAG context, and analytics collapse into one PK Table accessed through different views.', + basis: 'Unified substrate spanning structured features and vector context.', + Svg: require('@site/static/img/feature_query.svg').default, }, { - title: 'Changelogs & Decision Tracking', - content: - 'Built-in changelog generation provides an append-only history of state and decision evolution, enabling auditing, reproducibility, and deep system observability.', - Svg: require('@site/static/img/feature_changelog.svg').default + number: '06', + title: 'Ecosystem Openness', + summary: 'Open formats. No vendor lock-in.', + body: 'Readable by Flink, Spark, Trino, StarRocks, and DuckDB. Native hot tier plus Iceberg, Paimon, and Lance for the cold tier, open formats end to end.', + basis: 'Open lake formats throughout, governed at the Apache Software Foundation.', + Svg: require('@site/static/img/feature_changelog.svg').default, }, ]; -function Feature({ title, content, Svg }: FeatureItem) { +function PillarCard({number, title, summary, body, basis, Svg}: Pillar) { return ( -
-
- -
-
-
{title}
-
{content}
+
+
+ +
-
+ {title} +

{summary}

+

{body}

+

+ Architectural basis + {basis} +

+ ); } export default function HomepageFeatures(): JSX.Element { - return ( -
-
-
- Key Features -
-
- {FeatureList.map((props, idx) => ( - - ))} -
-
-
- ); + return ( +
+
+
+ Six capability pillars + + The benefits, grounded in the architecture. + +

+ Each pillar is a direct consequence of a specific architectural + mechanism. Together they collapse the fragmented real-time stack + into a single coherent foundation. +

+
+ +
+ {PILLARS.map((p) => ( + + ))} +
+
+
+ ); } diff --git a/website/src/components/HomepageFeatures/styles.module.css b/website/src/components/HomepageFeatures/styles.module.css index cbb3123740..d180bc19a2 100644 --- a/website/src/components/HomepageFeatures/styles.module.css +++ b/website/src/components/HomepageFeatures/styles.module.css @@ -15,52 +15,251 @@ * limitations under the License. */ +/* Always-dark homepage section. + * Pinned to deep-blue gradient (#0A1745 → #102856) for both light and + * dark mode, so the page reads as a single brutalist dark experience + * regardless of theme. Same cyan / violet / blue radial overlays as + * before — they just sit on a dark base now. */ .features { + position: relative; + padding: var(--fluss-space-24) 0; + background: + radial-gradient(1200px 520px at -5% -10%, rgba(38, 109, 149, 0.16), transparent 60%), + radial-gradient(900px 460px at 110% 110%, rgba(124, 58, 237, 0.14), transparent 60%), + radial-gradient(700px 360px at 50% 0%, rgba(28, 80, 120, 0.08), transparent 60%), + linear-gradient(180deg, #0A1745 0%, #102856 100%); + border-top: 1px solid rgba(255, 255, 255, 0.06); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + color: rgba(219, 234, 254, 0.88); +} + +/* Mesh lines now use translucent white instead of brand-blue alpha. */ +.features::before { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%); + -webkit-mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%); + pointer-events: none; +} + +.container { + position: relative; + z-index: 1; + max-width: var(--fluss-container-max); + margin: 0 auto; + padding: 0 var(--fluss-container-pad); +} + +.header { + text-align: left; + max-width: 720px; + margin-bottom: var(--fluss-space-16); +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px 14px; + border-radius: var(--fluss-radius-pill); + background: linear-gradient(120deg, rgba(38, 109, 149, 0.18) 0%, rgba(28, 80, 120, 0.14) 100%); + border: 1px solid rgba(122, 175, 203, 0.32); + color: #B1CEDF; + font-family: var(--fluss-font-display); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: var(--fluss-space-4); + box-shadow: 0 2px 8px rgba(38, 109, 149, 0.12); +} + +.eyebrow::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: linear-gradient(120deg, var(--fluss-blue-600), var(--fluss-cyan)); +} + +.heading { + font-family: var(--fluss-font-display); + font-size: clamp(2rem, 3vw + 1rem, 3rem); + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + text-wrap: balance; + color: #FFFFFF; + margin: 0 0 var(--fluss-space-4); +} + +.lead { + font-size: 1.125rem; + line-height: 1.65; + color: rgba(219, 234, 254, 0.78); + margin: 0; + text-wrap: pretty; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--fluss-space-4); +} + +@media (max-width: 996px) { + .grid { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 600px) { + .grid { grid-template-columns: 1fr; } +} + +/* Pillar card — translucent white over the deep-blue section bg. + * Same translucent-card pattern as .statCard / .communityCard. */ +.card { display: flex; + flex-direction: column; + gap: var(--fluss-space-3); + padding: var(--fluss-space-8) var(--fluss-space-6) var(--fluss-space-6); + background: + radial-gradient(420px 200px at 100% 0%, rgba(38, 109, 149, 0.12), transparent 60%), + rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--fluss-radius-lg); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.25); + position: relative; + overflow: hidden; + transition: transform var(--fluss-motion-base) var(--fluss-ease-out), + box-shadow var(--fluss-motion-base) var(--fluss-ease-out), + border-color var(--fluss-motion-base) var(--fluss-ease-out); +} + +.card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 3px; + background: linear-gradient(90deg, var(--fluss-blue-600), var(--fluss-cyan)); + opacity: 0; + transition: opacity var(--fluss-motion-base) var(--fluss-ease-out); +} + +.card::after { + content: ''; + position: absolute; + inset: -40% -40% auto auto; + width: 240px; + height: 240px; + border-radius: 50%; + background: radial-gradient(closest-side, rgba(38, 109, 149, 0.18), transparent); + opacity: 0; + transition: opacity var(--fluss-motion-base) var(--fluss-ease-out); + pointer-events: none; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(122, 175, 203, 0.32); + border-color: rgba(122, 175, 203, 0.32); +} + +.card:hover::before, +.card:hover::after { + opacity: 1; +} + +.cardTop { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--fluss-space-2); +} + +/* Icon container enlarged from 48px → 64px (and inner SVG 24 → 32) so the + six pillars are visually distinct at a glance (Jark feedback, PR #3226). */ +.iconWrap { + width: 64px; + height: 64px; + display: inline-flex; align-items: center; - padding: 2rem 0; - width: 100%; - background: #F7F9FE; -} - -.featureSvg { - height: 48px; - width: 48px; -} - -.core_features { - background-color: white; - padding: 2.5rem 2rem 3.375rem; - border-radius: 8px; - height: calc(100% - 5rem); - text-align: left; -} - -.core_features_title { - font-size: 1.25rem; - font-weight: 500; - line-height: 1.6; - margin-bottom: 0.75rem; -} - -.core_features_content { - font-size: 1rem; - font-weight: 400; - line-height: 1.875; -} - -.core_features_icon { - width: 4rem; - height: 4rem; - background-color: white; - position: relative; - transform: translateY(50%); - border-radius: 50%; - left: 1.5rem; - svg { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } + justify-content: center; + border-radius: var(--fluss-radius-md); + background: + linear-gradient(135deg, rgba(28, 80, 120, 0.20) 0%, rgba(38, 109, 149, 0.28) 100%); + color: #B1CEDF; + box-shadow: + inset 0 0 0 1px rgba(122, 175, 203, 0.32), + 0 4px 12px rgba(38, 109, 149, 0.18); +} + +.icon { + width: 32px; + height: 32px; +} + +.number { + font-family: var(--fluss-font-mono); + font-size: 0.8125rem; + font-weight: 600; + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.42); +} + +.title { + font-family: var(--fluss-font-display); + font-size: 1.375rem; + font-weight: 600; + letter-spacing: -0.01em; + text-wrap: balance; + color: #FFFFFF; + margin: 0; +} + +.summary { + font-size: 1.0625rem; + line-height: 1.5; + color: rgba(255, 255, 255, 0.92); + font-weight: 500; + margin: 0; + text-wrap: pretty; +} + +.body { + font-size: 0.9375rem; + line-height: 1.65; + color: rgba(219, 234, 254, 0.78); + margin: 0; + text-wrap: pretty; +} + +.basis { + /* `margin-top: auto` in a flex column pushes this block to the + bottom of the card. Combined with the grid's default stretch + alignment (so all cards in a row share the tallest card's height), + every "Architectural basis" section starts at the same horizontal + line across the row regardless of how long each card's body text + is. */ + margin: auto 0 0; + padding-top: var(--fluss-space-3); + border-top: 1px dashed rgba(255, 255, 255, 0.12); + font-size: 0.8125rem; + line-height: 1.55; + color: rgba(219, 234, 254, 0.70); +} + +.basisLabel { + display: block; + font-family: var(--fluss-font-display); + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #B1CEDF; + margin-bottom: 2px; } diff --git a/website/src/components/HomepageIntroduce/index.tsx b/website/src/components/HomepageIntroduce/index.tsx deleted file mode 100644 index 6d747f23dc..0000000000 --- a/website/src/components/HomepageIntroduce/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 clsx from 'clsx'; -import Heading from '@theme/Heading'; -import Link from '@docusaurus/Link'; -import styles from './styles.module.css'; - -type IntroduceItem = { - title: string; - description: JSX.Element; - Svg: React.ComponentType>; -}; - -const IntroduceList: IntroduceItem[] = [ - { - description: ( - <> - Apache Fluss (Incubating) is a streaming storage built for real-time analytics & AI which can serve as the real-time data layer for Lakehouse architectures. With its columnar stream and real-time update capabilities, Fluss integrates seamlessly with Apache Flink to enable high-throughput, low-latency, cost-effective streaming data warehouses tailored for real-time applications. - - ), - image: require('@site/static/img/fluss.png').default, - } -]; - - -function Introduce({title, description, image}: IntroduceItem) { - return ( -
-
- {title} -

{description}

-
-
- -
-
- ); -} - -export default function HomepageIntroduce(): JSX.Element { - return ( -
-
-
-
- {IntroduceList.map((props, idx) => ( - - ))} -
-
-
-
- ); -} diff --git a/website/src/components/HomepageIntroduce/styles.module.css b/website/src/components/HomepageIntroduce/styles.module.css deleted file mode 100644 index c61445411b..0000000000 --- a/website/src/components/HomepageIntroduce/styles.module.css +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - */ - -.introduce { - display: flex; - align-items: center; - padding: 2rem 0; - width: 100%; - - h1 { - font-size: 2.5rem; - } -} diff --git a/website/src/css/custom.css b/website/src/css/custom.css index eaa837553a..1067d18e59 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -21,74 +21,221 @@ * work well for content-centric websites. */ -/* Self-hosted fonts via @fontsource (no external requests) */ -@import '@fontsource/inter/400.css'; -@import '@fontsource/inter/500.css'; -@import '@fontsource/inter/600.css'; -@import '@fontsource/inter/700.css'; -@import '@fontsource/inter/800.css'; -@import '@fontsource/roboto/400.css'; -@import '@fontsource/roboto/500.css'; -@import '@fontsource/roboto/700.css'; - -/* You can override the default Infima variables here. */ +/* Self-hosted variable fonts via @fontsource (no external requests). + * Geist is OFL-licensed (https://vercel.com/font); the variable bundles + * carry the full 100-900 weight axis in a single woff2 per family, so + * picking weights becomes a styling decision rather than a load decision. */ +@import '@fontsource-variable/geist'; +@import '@fontsource-variable/geist-mono'; + +/* ========================================================================= + Fluss Design Tokens (2026 redesign) + ========================================================================= */ :root { - /* Use Roboto consistently across the platform */ - --ifm-font-family-base: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + /* --- Brand: Navy scale (2026 palette refresh) --- + Anchored on six user-provided palette stops: + #0A1745 · #102856 · #12325C · #194670 · #1C5078 · #266D95 + The 300/100/50 stops are derived lighter tints in the same hue family + so we keep enough light surfaces / light-text accents on dark grounds. */ + --fluss-blue-950: #0A1745; + --fluss-blue-900: #102856; + --fluss-blue-800: #12325C; + --fluss-blue-700: #194670; + --fluss-blue-600: #1C5078; + --fluss-blue-500: #266D95; + --fluss-blue-300: #7AAFCB; + --fluss-blue-100: #D6E4ED; + --fluss-blue-50: #ECF3F7; + + /* --- Accents (use sparingly) --- + The previous bright cyan is replaced with the palette's brightest stop + so accents stay within the new navy/teal family. */ + --fluss-cyan: #266D95; + --fluss-violet: #7C3AED; + --fluss-lime: #A3E635; + + /* --- Link (docs body prose) --- + A brighter, more saturated blue than the navy text/heading palette so + in-prose links visibly stand out from body copy ("QuickStart", + "Architecture", etc. on the docs intro page). Hover deepens to navy. */ + --fluss-link: #0E76C2; + --fluss-link-hover: #094E83; + + /* --- Neutrals --- */ + --fluss-ink-950: #0A0F1C; + --fluss-ink-700: #334155; + --fluss-ink-500: #64748B; + --fluss-ink-300: #CBD5E1; + --fluss-ink-100: #E6ECF5; + --fluss-paper: #FFFFFF; + --fluss-canvas: #FFFFFF; + --fluss-canvas-soft: #ECF3F7; + + /* --- Semantic --- */ + --fluss-success: #10B981; + --fluss-warning: #F59E0B; + --fluss-danger: #EF4444; + + /* --- Spacing (8px base) --- */ + --fluss-space-1: 4px; + --fluss-space-2: 8px; + --fluss-space-3: 12px; + --fluss-space-4: 16px; + --fluss-space-5: 20px; + --fluss-space-6: 24px; + --fluss-space-8: 32px; + --fluss-space-10: 40px; + --fluss-space-12: 48px; + --fluss-space-16: 64px; + --fluss-space-20: 80px; + --fluss-space-24: 96px; + --fluss-space-32: 128px; + + /* --- Radius --- */ + --fluss-radius-sm: 6px; + --fluss-radius-md: 10px; + --fluss-radius-lg: 14px; + --fluss-radius-xl: 20px; + --fluss-radius-2xl: 28px; + --fluss-radius-pill: 999px; + + /* --- Shadows (cool navy-tinted, palette-driven) --- */ + --fluss-shadow-sm: 0 1px 2px rgba(10, 23, 69, 0.06), 0 2px 6px rgba(10, 23, 69, 0.04); + --fluss-shadow-md: 0 6px 20px rgba(10, 23, 69, 0.10), 0 2px 6px rgba(10, 23, 69, 0.05); + --fluss-shadow-lg: 0 24px 60px rgba(10, 23, 69, 0.14), 0 6px 16px rgba(10, 23, 69, 0.06); + --fluss-shadow-glow: 0 0 0 1px rgba(28, 80, 120, 0.30), 0 16px 48px rgba(38, 109, 149, 0.28); - /* Increase line height for a more "airy" and professional feel */ - --ifm-line-height-base: 1.65; + /* --- Motion --- */ + --fluss-motion-fast: 120ms; + --fluss-motion-base: 240ms; + --fluss-motion-slow: 480ms; + --fluss-ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); + --fluss-ease-inout: cubic-bezier(0.4, 0, 0.2, 1); - /* Refine primary colors to a slightly more professional "Brand Blue" */ - --ifm-color-primary: #0062cc; - --ifm-color-primary-dark: #0056b3; - --ifm-color-primary-darker: #004d99; - --ifm-color-primary-darkest: #003d7a; - --ifm-color-primary-light: #1a7ae0; - --ifm-color-primary-lighter: #338df2; - --ifm-color-primary-lightest: #66abf7; + /* --- Typography --- */ + --fluss-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + --fluss-font-display: 'Inter', var(--fluss-font-sans); + --fluss-font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; - /* Adjust heading weights */ - --ifm-heading-font-weight: 700; + /* --- Layout --- */ + --fluss-container-max: 1240px; + --fluss-container-pad: 24px; + + /* ===================================================================== + Infima overrides : wire tokens into Docusaurus + ===================================================================== */ + --ifm-font-family-base: var(--fluss-font-sans); + --ifm-heading-font-family: var(--fluss-font-display); + --ifm-font-family-monospace: var(--fluss-font-mono); + --ifm-line-height-base: 1.6; + --ifm-heading-font-weight: 600; + --ifm-heading-color: var(--fluss-ink-950); + --ifm-font-color-base: var(--fluss-ink-700); + + --ifm-color-primary: #1C5078; + --ifm-color-primary-dark: #194670; + --ifm-color-primary-darker: #12325C; + --ifm-color-primary-darkest: #102856; + --ifm-color-primary-light: #266D95; + --ifm-color-primary-lighter: #7AAFCB; + --ifm-color-primary-lightest: #D6E4ED; --ifm-code-font-size: 90%; - --docusaurus-highlighted-code-line-bg: #E2E9F3; + --docusaurus-highlighted-code-line-bg: #DDE6EE; + + --ifm-menu-color-background-active: #ECF3F7; + --ifm-menu-color-background-hover: #ECF3F7; + + /* Navbar: a single dark Fluss palette across the WHOLE site (homepage, + docs, blog, community) so the navbar reads identically on every + page — no per-route transparent / frosted variants. + Background uses the deepest palette stop (#0A1745) for a quiet, + premium feel. */ + --ifm-navbar-background-color: rgba(10, 23, 69, 0.92); + --ifm-navbar-link-color: #CBD5E1; + --ifm-navbar-link-hover-color: #FFFFFF; + --ifm-navbar-shadow: 0 1px 0 rgba(255, 255, 255, 0.06); + --ifm-footer-background-color: var(--fluss-blue-950); + --ifm-footer-color: var(--fluss-ink-300); + --ifm-footer-link-color: var(--fluss-ink-300); + --ifm-footer-title-color: #FFFFFF; +} + +/* ========================================================================= + Base typographic refinement + ========================================================================= */ +html { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Match the body bg so the browser's default white html surface + * doesn't show as a thin line at the top during overscroll / before + * the body paint. The variable flips with theme via tokens above. */ + background: var(--fluss-canvas); +} + +body { + background: var(--fluss-canvas); +} + +/* Page scrollbar — keep the thumb bright in dark mode so it stays + * visible against the deep-blue canvas instead of fading to grey + * via the browser's auto-dark heuristic. + * `scrollbar-color` — Firefox / future-spec + * `::-webkit-scrollbar-*` — Chromium / Safari + * Thumb / track in BOTH modes get an explicit treatment so the page + * scrollbar reads consistently. */ +[data-theme='dark'] html { + scrollbar-color: rgba(255, 255, 255, 0.42) rgba(255, 255, 255, 0.06); +} +[data-theme='dark'] body::-webkit-scrollbar { + width: 12px; + height: 12px; +} +[data-theme='dark'] body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.04); +} +[data-theme='dark'] body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.32); + border-radius: 8px; + border: 3px solid transparent; + background-clip: padding-box; +} +[data-theme='dark'] body::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.55); + background-clip: padding-box; +} - --ifm-menu-color-background-active: #edeefa99; - --ifm-menu-color-background-hover: #edeefa99; +/* Subtle dot-grid texture on the homepage canvas in light mode. + Mirrors the dark-mode docs-canvas treatment so the page reads as + "designed" instead of flat white. The pattern fades behind hero + and dark sections because they paint their own backgrounds. */ +body.fluss-home { + background-color: var(--fluss-canvas); + background-image: + radial-gradient(circle at 1px 1px, rgba(28, 80, 120, 0.10) 1px, transparent 0); + background-size: 28px 28px; + background-attachment: fixed; } +h1, h2, h3, h4, h5, h6 { + letter-spacing: -0.011em; +} .source_code_button { margin-left: 20px; } - +/* ========================================================================= + Navbar + ========================================================================= */ .navbar__brand { - font-family: 'Roboto', sans-serif; + font-family: var(--fluss-font-display); font-weight: 700; letter-spacing: -0.01em; color: inherit; } -.hero__title { - font-family: 'Roboto', sans-serif; - font-weight: 700; - font-size: 3.5rem; - letter-spacing: -0.02em; - line-height: 1.1; -} - -.hero__subtitle { - font-family: 'Roboto', sans-serif; - font-weight: 400; - font-size: 1.5rem; - opacity: 0.9; - max-width: 800px; - margin: 0 auto; - padding-bottom: 2rem; -} - .header-github-link:hover { opacity: 0.6; } @@ -98,11 +245,13 @@ width: 24px; height: 24px; 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.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23E2E8F0' 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.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; } - +/* ========================================================================= + Sidebar / menu + ========================================================================= */ .menu__list-item { font-size: 0.95rem; font-weight: 500; @@ -110,7 +259,7 @@ .menu__link { font-weight: 500; - color: #57606a; + color: var(--fluss-ink-500); } .menu__link--active { @@ -122,39 +271,35 @@ background: var(--ifm-menu-link-sublist-icon) 50% / 1.5rem 1.5rem; } - +/* ========================================================================= + Markdown content + ========================================================================= */ .markdown { padding-left: 1rem; - h1, - h2, - h3, - h4, - h5, - h6 { - color: #1d1d1d; + h1, h2, h3, h4, h5, h6 { + color: var(--fluss-ink-950); margin-bottom: 0.3125rem; - font-weight: 700; + font-weight: 600; } - b, - strong { + b, strong { font-weight: 700; - color: #1d1d1d; + color: var(--fluss-ink-950); } - h1, - h1:first-child { + h1, h1:first-child { font-size: 2.5rem; margin-bottom: 1.5rem; margin-top: 0; } h2 { - font-size: 2rem; - margin-bottom: 1.25rem; - margin-top: 2rem; - padding-top: 2rem; - border-top: 1px solid #e6e7e9; + font-size: 1.875rem; + margin-bottom: 1rem; + margin-top: 2.5rem; + padding-top: 0; + border-top: none; + letter-spacing: -0.018em; } h3 { @@ -162,8 +307,9 @@ margin-bottom: 1.25rem; margin-top: 1rem; } + p { - line-height: 1.875; + line-height: 1.8; code { border-radius: 4px; @@ -280,16 +426,47 @@ background: var(--ifm-menu-color-background-active); } +/* ========================================================================= + Footer + ========================================================================= */ +.footer { + padding: var(--fluss-space-20) 0 var(--fluss-space-12); +} + +.footer__title { + font-family: var(--fluss-font-display); + font-weight: 600; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #FFFFFF; + margin-bottom: var(--fluss-space-4); +} + +.footer__link-item { + color: #FFFFFF; + opacity: 0.88; + transition: opacity var(--fluss-motion-fast) var(--fluss-ease-out), color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.footer__link-item:hover { + color: #FFFFFF; + opacity: 1; + text-decoration: none; +} + .footer__copyright { - color: #dfe5f0; + color: #FFFFFF; font-size: .75rem; line-height: 1.8; - opacity: .6; + opacity: .78; text-align: center; width: 98%; } -/* Hide blog sidebar on individual blog post pages to increase content width */ +/* ========================================================================= + Blog post layout + ========================================================================= */ .blog-post-page-no-sidebar aside.col.col--3 { display: none; } @@ -304,6 +481,2819 @@ max-width: calc(3 / 12 * 100%); } +/* Blog list page: widen the main column from col--7 to col--9 + so that cards (next to the col--3 sidebar) get more room. */ +.blog-list-page main.col.col--7 { + flex-basis: calc(9 / 12 * 100%); + max-width: calc(9 / 12 * 100%); +} + +/* MDX pages (e.g. /downloads, /learn/talks, /learn/videos): widen the + right-side TOC column from col--2 to col--3 for better readability. */ +.mdx-page .col.col--2 { + flex-basis: calc(3 / 12 * 100%); + max-width: calc(3 / 12 * 100%); +} + .hidden { display: none !important; } + +/* ========================================================================= + Kapa AI widget hide rules + ========================================================================= */ +/* Hide every trigger element Kapa injects into its container so the only + visible AI entry point is the custom .navbar-ask-ai pill (defined below). + Kapa periodically changes its injected DOM, so we use a broad rule and + then re-show the actual modal dialog explicitly. */ +#kapa-widget-container > button, +#kapa-widget-container > div:not([role="dialog"]), +#kapa-widget-container > a { + display: none !important; +} + +/* Belt-and-braces: make sure the modal dialog (when open) is never hidden. */ +#kapa-widget-container > div[role="dialog"] { + display: revert !important; +} + +/* ========================================================================= + "Ask AI" navbar pill + ========================================================================= */ +.navbar-ask-ai { + position: relative; + display: inline-flex; + align-items: center; + gap: 7px; + margin: 0 8px; + padding: 7px 16px; + border: 0; + border-radius: var(--fluss-radius-pill); + background: linear-gradient(120deg, var(--fluss-cyan) 0%, var(--fluss-blue-600) 50%, var(--fluss-blue-500) 100%); + background-size: 180% 180%; + color: #ffffff; + font-family: inherit; + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.02em; + cursor: pointer; + box-shadow: 0 2px 10px rgba(6, 182, 212, 0.45), + 0 0 0 1px rgba(255, 255, 255, 0.1) inset; + animation: navbar-ask-ai-shimmer 5s ease-in-out infinite, + navbar-ask-ai-pulse 2.6s ease-in-out infinite; + transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease; +} + +.navbar-ask-ai::before { + content: ''; + width: 14px; + height: 14px; + background-color: #ffffff; + -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.5 L13.6 8.4 L20.5 10 L13.6 11.6 L12 18.5 L10.4 11.6 L3.5 10 L10.4 8.4 Z M19 15 L19.8 17.2 L22 18 L19.8 18.8 L19 21 L18.2 18.8 L16 18 L18.2 17.2 Z' fill='white'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.5 L13.6 8.4 L20.5 10 L13.6 11.6 L12 18.5 L10.4 11.6 L3.5 10 L10.4 8.4 Z M19 15 L19.8 17.2 L22 18 L19.8 18.8 L19 21 L18.2 18.8 L16 18 L18.2 17.2 Z' fill='white'/%3E%3C/svg%3E"); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; + flex-shrink: 0; +} + +.navbar-ask-ai:hover { + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(6, 182, 212, 0.65), + 0 0 0 1px rgba(255, 255, 255, 0.18) inset; + filter: brightness(1.08) saturate(1.1); + animation-play-state: paused; +} + +.navbar-ask-ai:focus-visible { + outline: 2px solid #ffffff; + outline-offset: 2px; + box-shadow: 0 0 0 4px rgba(6, 182, 212, 0.5); +} + +@keyframes navbar-ask-ai-shimmer { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +@keyframes navbar-ask-ai-pulse { + 0%, 100% { box-shadow: 0 2px 10px rgba(6, 182, 212, 0.45), + 0 0 0 1px rgba(255, 255, 255, 0.1) inset; } + 50% { box-shadow: 0 2px 18px rgba(6, 182, 212, 0.75), + 0 0 0 1px rgba(255, 255, 255, 0.14) inset; } +} + +@media (prefers-reduced-motion: reduce) { + .navbar-ask-ai { animation: none; } +} + +@media (max-width: 996px) { + .navbar-ask-ai { + padding: 5px 10px; + font-size: 0.85rem; + } +} + +/* ========================================================================= + Global focus ring (accessibility) + ========================================================================= */ +*:focus-visible { + outline: 2px solid var(--fluss-blue-500); + outline-offset: 2px; + border-radius: var(--fluss-radius-sm); +} + +/* ========================================================================= + Reduced-motion safety net + ========================================================================= */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } +} + +/* ========================================================================= + ======================================================================== + SITE-WIDE CHROME (docs, blog, community, learn, 404) + Below this line: theming for everything that is NOT the homepage. + ======================================================================== + ========================================================================= */ + +/* ------------------------------------------------------------------------- + Navbar (non-homepage solid state) + ------------------------------------------------------------------------- + Previously had a hard 1px white separator below the navbar which read + as a disconnect between the chrome and the page body (Jark feedback, + PR #3226). Replaced with a subtle drop shadow so the navbar still has + visual separation but flows into the page content. */ +.navbar { + height: 64px; + border-bottom: 0; + box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04), 0 2px 8px rgba(15, 23, 42, 0.03); +} + +/* Navbar inner now spans the full viewport width on every page, with the + same side padding as page content. Previously capped at 1240px, which + left the navbar items visibly inset on docs and other full-width pages + (Jark feedback, PR #3226). Same treatment on the homepage so the + navbar is visually consistent across the whole site. */ +.navbar__inner { + max-width: none; + margin: 0; + padding: 0 var(--fluss-container-pad); +} + +.navbar__items { + gap: 2px; +} + +.navbar__link { + font-size: 0.9375rem; + font-weight: 500; + color: var(--fluss-ink-700); + padding: 0 12px; + border-radius: var(--fluss-radius-md); + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.navbar__link:hover { + color: var(--fluss-blue-500); + text-decoration: none; +} + +.navbar__link--active { + color: var(--fluss-blue-500); + font-weight: 600; +} + +.navbar__title { + font-family: var(--fluss-font-display); + font-weight: 700; + letter-spacing: -0.01em; + color: var(--fluss-ink-950); +} + +/* Dropdown menus */ +.dropdown__menu { + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-lg); + box-shadow: var(--fluss-shadow-lg); + padding: 8px; + min-width: 200px; + background: var(--fluss-paper); +} + +.dropdown__link { + border-radius: var(--fluss-radius-sm); + padding: 8px 12px; + font-size: 0.9375rem; + font-weight: 500; + color: var(--fluss-ink-700); + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.dropdown__link:hover, +.dropdown__link--active { + color: var(--fluss-blue-700); + background: var(--fluss-blue-50); +} + +/* Algolia DocSearch button */ +.DocSearch-Button { + height: 36px; + padding: 0 12px; + border-radius: var(--fluss-radius-md); + background: var(--fluss-ink-100); + border: 1px solid transparent; + transition: border-color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.DocSearch-Button:hover { + border-color: var(--fluss-blue-300); + background: var(--fluss-paper); + box-shadow: none; +} + +.DocSearch-Button-Placeholder { + font-size: 0.875rem; + color: var(--fluss-ink-500); +} + +.DocSearch-Search-Icon { + color: var(--fluss-ink-500); +} + +/* Unify right-side navbar spacing so theme-toggle, search button, version + dropdown and GitHub icon align on a single baseline with consistent gap + (Michael feedback, PR #3226). + + `!important` is required on the margin reset to defeat the + `button[class*='colorModeToggle']` selector below, which would + otherwise re-introduce a stray horizontal margin on the toggle and + throw off the gap. */ +.navbar__items--right { + gap: var(--fluss-space-2); + align-items: center; +} + +.navbar__items--right > * { + margin: 0 !important; +} + +/* Tighten the version dropdown link's horizontal padding when it lives in + the right-side cluster, so the visible gap between "Next ▾" and the + GitHub icon matches the gap between every other pair of items. */ +.navbar__items--right .navbar__link { + padding: 0 4px; +} + +/* Version dropdown badge */ +.navbar .navbar__item--version, +.navbar .badge { + font-family: var(--fluss-font-display); + font-size: 0.8125rem; + font-weight: 600; +} + +/* ------------------------------------------------------------------------- + Mobile navbar drawer + ------------------------------------------------------------------------- + The drawer is a light surface (var(--fluss-paper)), so inherited styles + from the dark navbar — the white Fluss wordmark, the light-tinted GitHub + icon, and the #E2E8F0 navbar link colour — would all be invisible inside + it. Re-tint everything for the light drawer below (borzoni feedback, + PR #3226). */ +.navbar-sidebar { + background: var(--fluss-paper); +} + +.navbar-sidebar__brand { + border-bottom: 1px solid var(--fluss-ink-100); + background: var(--fluss-paper); +} + +.navbar-sidebar__items .menu__link { + font-size: 0.9375rem; +} + +/* Drawer brand: re-tint the white wordmark to brand navy (≈ --fluss-blue-600) + so it reads on the light paper background. The leading `brightness(0)` + collapses the source to black, the rest of the chain shifts it to the + target hue. */ +.navbar-sidebar__brand .navbar__logo img { + filter: brightness(0) saturate(100%) invert(13%) sepia(53%) saturate(2243%) hue-rotate(193deg) brightness(96%) contrast(95%); +} + +.navbar-sidebar__brand .navbar__title, +.navbar-sidebar__brand .navbar__brand { + color: var(--fluss-ink-950); +} + +/* Drawer navbar links (e.g. items dropped into the drawer on mobile) + inherit color: #E2E8F0 from the dark navbar. Re-tint for the light + drawer so the labels are readable. */ +.navbar-sidebar .navbar__link, +.navbar-sidebar .menu__link { + color: var(--fluss-ink-700); +} + +.navbar-sidebar .navbar__link:hover, +.navbar-sidebar .menu__link:hover { + color: var(--fluss-blue-700); +} + +.navbar-sidebar .navbar__link--active, +.navbar-sidebar .menu__link--active { + color: var(--fluss-blue-700); +} + +/* GitHub icon in the drawer: the navbar variant uses fill='#E2E8F0', + which vanishes on the light drawer. Swap to a brand-blue fill. The + `.navbar-sidebar` ancestor selector already wins on specificity, no + `!important` needed. */ +.navbar-sidebar .header-github-link::before { + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%231C5078' 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.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; +} + +/* Drawer close button (the X) — keep it readable on the light surface. */ +.navbar-sidebar__close { + color: var(--fluss-ink-700); +} + +.navbar-sidebar__close:hover { + color: var(--fluss-blue-700); +} + +/* ------------------------------------------------------------------------- + Announcement bar + ------------------------------------------------------------------------- */ +div[class*='announcementBar'] { + background: linear-gradient(120deg, var(--fluss-blue-700) 0%, var(--fluss-blue-600) 100%); + color: #FFFFFF; + font-weight: 500; + border-bottom: none; +} + +div[class*='announcementBar'] a { + color: var(--fluss-blue-100); + text-decoration: underline; +} + +/* ------------------------------------------------------------------------- + Doc page layout : sidebar / main / TOC + ------------------------------------------------------------------------- */ +.theme-doc-sidebar-container { + border-right: 1px solid var(--fluss-ink-100); + background: var(--fluss-canvas-soft); +} + +.theme-doc-sidebar-menu { + padding: var(--fluss-space-3) var(--fluss-space-2); +} + +.menu { + background: transparent; + font-size: 0.9375rem; +} + +.menu__list .menu__list { + margin-left: 8px; + padding-left: 8px; + border-left: 1px solid var(--fluss-ink-100); +} + +.menu__link { + border-radius: var(--fluss-radius-sm); + padding: 6px 10px; + font-weight: 500; + color: var(--fluss-ink-700); + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.menu__link:hover { + background: var(--fluss-blue-50); + color: var(--fluss-blue-700); +} + +.menu__link--active { + color: var(--fluss-blue-700); + background: var(--fluss-blue-50); + font-weight: 600; +} + +.menu__link--sublist { + font-weight: 600; + color: var(--fluss-ink-950); +} + +.menu__caret:hover { + background: var(--fluss-blue-50); +} + +/* Sidebar toggle button at the bottom */ +.theme-doc-sidebar-container button { + border-color: var(--fluss-ink-100); + color: var(--fluss-ink-500); + border-radius: var(--fluss-radius-sm); +} + +.theme-doc-sidebar-container button:hover { + border-color: var(--fluss-blue-300); + background: var(--fluss-blue-50); +} + +/* ------------------------------------------------------------------------- + Doc breadcrumbs + ------------------------------------------------------------------------- */ +.breadcrumbs__link { + font-size: 0.875rem; + font-weight: 500; + color: var(--fluss-ink-500); + border-radius: var(--fluss-radius-sm); + padding: 4px 8px; + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +/* Anchor-only hover styles. Category breadcrumb segments without a target + page render as a plain element (no ); those should not look clickable + (borzoni feedback, PR #3226). */ +a.breadcrumbs__link:hover { + color: var(--fluss-blue-700); + background: var(--fluss-blue-50); +} + +/* Non-link breadcrumb segment (parent category without an index page): + render as static text so users don't try to click it. */ +.breadcrumbs__link:not(a) { + color: var(--fluss-ink-700); + cursor: default; +} + +.breadcrumbs__item--active .breadcrumbs__link { + color: var(--fluss-ink-950); + font-weight: 600; + background: var(--fluss-ink-100); +} + +.breadcrumbs__item:not(:last-child)::after { + color: var(--fluss-ink-300); +} + +/* ------------------------------------------------------------------------- + Table of contents (right rail) + ------------------------------------------------------------------------- */ +.table-of-contents { + border-left: 1px solid var(--fluss-ink-100); + font-size: 0.8125rem; + padding-left: var(--fluss-space-3); +} + +.table-of-contents__title, +.table-of-contents > *:first-child { + font-family: var(--fluss-font-display); + font-weight: 600; + font-size: 0.75rem; + letter-spacing: 0.06em; + color: var(--fluss-ink-500); + margin-bottom: var(--fluss-space-3); +} + +.table-of-contents__link { + color: var(--fluss-ink-500); + font-weight: 500; + padding: 4px 0; + display: block; + transition: color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.table-of-contents__link:hover { + color: var(--fluss-blue-700); + text-decoration: none; +} + +.table-of-contents__link--active { + color: var(--fluss-blue-700); + font-weight: 600; +} + +/* ------------------------------------------------------------------------- + Doc content typography polish + ------------------------------------------------------------------------- */ +.theme-doc-markdown { + font-size: 1rem; + line-height: 1.7; + color: var(--fluss-ink-700); +} + +.theme-doc-markdown a:not(.button):not(.card) { + color: var(--fluss-blue-700); + text-decoration-color: rgba(28, 80, 120, 0.3); + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: text-decoration-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.theme-doc-markdown a:not(.button):not(.card):hover { + text-decoration-color: var(--fluss-blue-700); +} + +.markdown a.hash-link { + color: var(--fluss-ink-300); + text-decoration: none; + transition: color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.markdown a.hash-link:hover { + color: var(--fluss-blue-600); +} + +/* Doc page title (h1) : refined */ +header.theme-doc-markdown h1, +.theme-doc-markdown > article > header > h1 { + font-family: var(--fluss-font-display); + font-size: 2.5rem; + font-weight: 700; + letter-spacing: -0.022em; + line-height: 1.1; + color: var(--fluss-ink-950); +} + +/* "Edit this page" / last updated row */ +.theme-doc-footer { + margin-top: var(--fluss-space-12); + padding-top: var(--fluss-space-6); + border-top: 1px solid var(--fluss-ink-100); +} + +.theme-edit-this-page { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--fluss-blue-700); + font-weight: 500; + font-size: 0.875rem; +} + +.theme-edit-this-page:hover { + color: var(--fluss-blue-600); + text-decoration: underline; +} + +.theme-last-updated { + font-size: 0.8125rem; + color: var(--fluss-ink-500); +} + +/* ------------------------------------------------------------------------- + Pagination (prev / next at the bottom of doc pages) + ------------------------------------------------------------------------- */ +.pagination-nav__link { + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-lg); + padding: var(--fluss-space-4) var(--fluss-space-5); + background: var(--fluss-paper); + transition: transform var(--fluss-motion-base) var(--fluss-ease-out), + border-color var(--fluss-motion-base) var(--fluss-ease-out), + box-shadow var(--fluss-motion-base) var(--fluss-ease-out); +} + +.pagination-nav__link:hover { + border-color: var(--fluss-blue-300); + box-shadow: var(--fluss-shadow-sm); + transform: translateY(-1px); + text-decoration: none; +} + +.pagination-nav__sublabel { + font-family: var(--fluss-font-display); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fluss-ink-500); + margin-bottom: 4px; +} + +.pagination-nav__label { + color: var(--fluss-ink-950); + font-weight: 600; + font-size: 1rem; +} + +/* ------------------------------------------------------------------------- + Admonitions (note, tip, info, warning, danger, caution) + ------------------------------------------------------------------------- */ +.theme-admonition, +.alert, +[class*='admonition_'] { + border-radius: var(--fluss-radius-lg); + border: 1px solid var(--fluss-ink-100); + border-left-width: 4px; + background: var(--fluss-paper); + padding: var(--fluss-space-4) var(--fluss-space-5); + box-shadow: var(--fluss-shadow-sm); + margin: var(--fluss-space-6) 0; +} + +.alert--secondary, +.theme-admonition-note { + background: var(--fluss-ink-100); + border-color: var(--fluss-ink-300); + border-left-color: var(--fluss-ink-500); +} + +.alert--info, +.theme-admonition-info { + background: var(--fluss-blue-50); + border-color: var(--fluss-blue-300); + border-left-color: var(--fluss-blue-600); +} + +.alert--success, +.theme-admonition-tip { + background: #ECFDF5; + border-color: #A7F3D0; + border-left-color: var(--fluss-success); +} + +.alert--warning, +.theme-admonition-warning, +.theme-admonition-caution { + background: #FFFBEB; + border-color: #FCD34D; + border-left-color: var(--fluss-warning); +} + +.alert--danger, +.theme-admonition-danger { + background: #FEF2F2; + border-color: #FCA5A5; + border-left-color: var(--fluss-danger); +} + +/* Admonition heading row */ +[class*='admonitionHeading'] { + font-family: var(--fluss-font-display); + font-weight: 600; + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 6px; + color: var(--fluss-ink-950); +} + +/* ------------------------------------------------------------------------- + Inline code and code blocks + ------------------------------------------------------------------------- */ +code { + font-family: var(--fluss-font-mono); + font-size: 0.875em; + background: var(--fluss-blue-50); + border: 1px solid rgba(28, 80, 120, 0.12); + color: var(--fluss-blue-800); + padding: 1px 6px; + border-radius: var(--fluss-radius-sm); +} + +a code, +.theme-doc-markdown a code { + color: inherit; + background: rgba(28, 80, 120, 0.12); + border-color: transparent; +} + +/* Code block container */ +.theme-code-block, +div[class*='codeBlockContainer'] { + border-radius: var(--fluss-radius-lg); + border: 1px solid var(--fluss-ink-100); + box-shadow: var(--fluss-shadow-sm); + overflow: hidden; + margin: var(--fluss-space-5) 0; +} + +div[class*='codeBlockTitle'] { + background: var(--fluss-ink-100); + border-bottom: 1px solid var(--fluss-ink-300); + font-family: var(--fluss-font-mono); + font-size: 0.8125rem; + color: var(--fluss-ink-700); + padding: 8px 14px; +} + +div[class*='codeBlockContent'] pre { + background: #F8FAFC; +} + +pre[class*='language-'] { + font-size: 0.875rem; + line-height: 1.65; + padding: var(--fluss-space-4) var(--fluss-space-5); +} + +/* Copy-to-clipboard button */ +button[class*='copyButton'] { + background: var(--fluss-paper); + border: 1px solid var(--fluss-ink-100); + color: var(--fluss-ink-500); + border-radius: var(--fluss-radius-sm); + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +button[class*='copyButton']:hover { + color: var(--fluss-blue-700); + background: var(--fluss-blue-50); + border-color: var(--fluss-blue-300); +} + +/* Word-wrap toggle */ +button[class*='wordWrapButton'] { + background: var(--fluss-paper); + border: 1px solid var(--fluss-ink-100); + color: var(--fluss-ink-500); + border-radius: var(--fluss-radius-sm); +} + +/* Highlighted lines */ +.theme-code-block-highlighted-line, +.docusaurus-highlight-code-line { + background: rgba(28, 80, 120, 0.08); + border-left: 3px solid var(--fluss-blue-600); + padding-left: calc(var(--fluss-space-5) - 3px); + display: block; + margin: 0 calc(-1 * var(--fluss-space-5)); +} + +/* Language label corner */ +div[class*='codeBlockTitle']::before { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--fluss-cyan); + margin-right: 8px; + vertical-align: middle; +} + +/* ------------------------------------------------------------------------- + Tabs (used in docs for SQL / Java / etc. examples) + ------------------------------------------------------------------------- */ +.tabs { + border-bottom: 1px solid var(--fluss-ink-100); + margin-bottom: 0; + gap: 4px; +} + +.tabs__item { + font-family: var(--fluss-font-display); + font-size: 0.875rem; + font-weight: 600; + color: var(--fluss-ink-500); + padding: 10px 14px; + border-radius: var(--fluss-radius-sm) var(--fluss-radius-sm) 0 0; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.tabs__item:hover { + background: var(--fluss-blue-50); + color: var(--fluss-blue-700); +} + +.tabs__item--active { + color: var(--fluss-blue-700); + border-bottom-color: var(--fluss-blue-600); + background: transparent; +} + +/* ------------------------------------------------------------------------- + Details / Summary (collapsibles) + ------------------------------------------------------------------------- */ +details { + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-md); + background: var(--fluss-paper); + padding: var(--fluss-space-3) var(--fluss-space-4); + margin: var(--fluss-space-4) 0; +} + +details > summary { + cursor: pointer; + font-weight: 600; + color: var(--fluss-ink-950); + font-family: var(--fluss-font-display); + list-style: none; + display: flex; + align-items: center; + gap: 8px; +} + +details > summary::-webkit-details-marker { + display: none; +} + +details > summary::before { + content: ''; + width: 8px; + height: 8px; + border-right: 2px solid var(--fluss-ink-500); + border-bottom: 2px solid var(--fluss-ink-500); + transform: rotate(-45deg); + transition: transform var(--fluss-motion-base) var(--fluss-ease-out); +} + +details[open] > summary::before { + transform: rotate(45deg); +} + +/* ------------------------------------------------------------------------- + Headings : anchor offset for sticky navbar + ------------------------------------------------------------------------- */ +.theme-doc-markdown h1, +.theme-doc-markdown h2, +.theme-doc-markdown h3, +.theme-doc-markdown h4 { + scroll-margin-top: 80px; +} + +/* ------------------------------------------------------------------------- + Doc cards (used in some index pages) + ------------------------------------------------------------------------- */ +article.card, +.theme-doc-card { + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-lg); + background: var(--fluss-paper); + box-shadow: var(--fluss-shadow-sm); + transition: transform var(--fluss-motion-base) var(--fluss-ease-out), + box-shadow var(--fluss-motion-base) var(--fluss-ease-out), + border-color var(--fluss-motion-base) var(--fluss-ease-out); +} + +article.card:hover, +.theme-doc-card:hover { + transform: translateY(-2px); + box-shadow: var(--fluss-shadow-md); + border-color: var(--fluss-blue-300); +} + +/* ------------------------------------------------------------------------- + Version banner (unmaintained / unreleased) + ------------------------------------------------------------------------- + Previously rendered as a centred rounded card, which on docs pages + (full-width main column) sat visibly inset from the page edges and + read as disconnected (Jark feedback, PR #3226). Now rendered as a + full-bleed strip across the doc content column: no rounded corners, + no side border, top/bottom borders only — aligns with the page chrome + above and below it. */ +div[class*='docVersionBanner'] { + border: 0; + border-top: 1px solid var(--fluss-blue-300); + border-bottom: 1px solid var(--fluss-blue-300); + border-radius: 0; + background: var(--fluss-blue-50); + color: var(--fluss-ink-950); + padding: var(--fluss-space-3) var(--fluss-space-5); + margin: 0 0 var(--fluss-space-6); + font-size: 0.9375rem; +} + +div[class*='docVersionBanner'] b { + color: var(--fluss-blue-700); +} + +div[class*='docVersionBanner'] a { + color: var(--fluss-blue-700); + font-weight: 600; +} + +/* ------------------------------------------------------------------------- + Blog : list page (cards) + ------------------------------------------------------------------------- */ +.blog-list-page article, +article[itemtype='https://schema.org/BlogPosting'] { + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-lg); + padding: var(--fluss-space-8); + background: var(--fluss-paper); + margin-bottom: var(--fluss-space-6); + box-shadow: var(--fluss-shadow-sm); + transition: transform var(--fluss-motion-base) var(--fluss-ease-out), + box-shadow var(--fluss-motion-base) var(--fluss-ease-out), + border-color var(--fluss-motion-base) var(--fluss-ease-out); +} + +.blog-list-page article:hover, +article[itemtype='https://schema.org/BlogPosting']:hover { + transform: translateY(-2px); + box-shadow: var(--fluss-shadow-md); + border-color: var(--fluss-blue-300); +} + +article[itemtype='https://schema.org/BlogPosting'] header h2 a, +.blog-list-page article header h2 a { + font-family: var(--fluss-font-display); + font-weight: 700; + color: var(--fluss-ink-950); + letter-spacing: -0.01em; + text-decoration: none; +} + +article[itemtype='https://schema.org/BlogPosting'] header h2 a:hover, +.blog-list-page article header h2 a:hover { + color: var(--fluss-blue-700); +} + +/* Blog post meta line */ +.blog-post-page-no-sidebar .blog-post-page, +article[itemtype='https://schema.org/BlogPosting'] header > div { + color: var(--fluss-ink-500); + font-size: 0.875rem; +} + +/* Blog tags */ +a.tag, +a[class*='tag_'] { + display: inline-flex; + align-items: center; + height: 26px; + padding: 0 10px; + border-radius: var(--fluss-radius-pill); + background: var(--fluss-blue-50); + border: 1px solid transparent; + color: var(--fluss-blue-700); + font-size: 0.8125rem; + font-weight: 600; + text-decoration: none; + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +a.tag:hover, +a[class*='tag_']:hover { + background: var(--fluss-blue-100); + border-color: var(--fluss-blue-300); + color: var(--fluss-blue-800); + text-decoration: none; +} + +/* Blog "Read more" button */ +.button.button--secondary { + background: var(--fluss-blue-600); + color: #FFFFFF; + border: none; + border-radius: var(--fluss-radius-md); + font-weight: 600; + padding: 10px 16px; + box-shadow: var(--fluss-shadow-sm); + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + transform var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.button.button--secondary:hover { + background: var(--fluss-blue-700); + color: #FFFFFF; + transform: translateY(-1px); +} + +/* ------------------------------------------------------------------------- + Blog sidebar (recent posts) + ------------------------------------------------------------------------- */ +aside.col[class*='blogSidebar'], +nav[class*='sidebar'] aside { + font-size: 0.875rem; +} + +nav[class*='sidebar'] h3 { + font-family: var(--fluss-font-display); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--fluss-ink-500); + margin-bottom: var(--fluss-space-3); +} + +/* ------------------------------------------------------------------------- + Blog authors row + ------------------------------------------------------------------------- */ +[class*='authorPhoto'] img, +[class*='authorAvatar'] img { + border: 2px solid var(--fluss-paper); + box-shadow: 0 0 0 1px var(--fluss-ink-100); +} + +/* ------------------------------------------------------------------------- + Blog post page header + ------------------------------------------------------------------------- */ +.blog-post-page-no-sidebar h1, +.blog-post-page article > header h1 { + font-family: var(--fluss-font-display); + font-size: clamp(2rem, 3vw + 1rem, 3rem); + font-weight: 700; + letter-spacing: -0.022em; + line-height: 1.1; + color: var(--fluss-ink-950); +} + +/* ------------------------------------------------------------------------- + Markdown tables in docs / blog (override Infima default for dark headers) + ------------------------------------------------------------------------- */ +.theme-doc-markdown table, +.markdown table { + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-md); + overflow: hidden; + box-shadow: var(--fluss-shadow-sm); +} + +.theme-doc-markdown thead, +.markdown thead { + background: var(--fluss-blue-50); +} + +.theme-doc-markdown thead th, +.markdown thead th { + font-family: var(--fluss-font-display); + font-weight: 600; + color: var(--fluss-ink-950); + border-color: var(--fluss-blue-100); +} + +.theme-doc-markdown tbody tr:nth-child(even), +.markdown tbody tr:nth-child(even) { + background: var(--fluss-blue-50); +} + +/* ------------------------------------------------------------------------- + Block-quote + ------------------------------------------------------------------------- */ +.theme-doc-markdown blockquote, +.markdown blockquote { + border-left: 3px solid var(--fluss-blue-600); + background: var(--fluss-blue-50); + border-radius: var(--fluss-radius-md); + padding: var(--fluss-space-4) var(--fluss-space-5); + color: var(--fluss-ink-700); + margin: var(--fluss-space-5) 0; +} + +/* ------------------------------------------------------------------------- + 404 page + ------------------------------------------------------------------------- */ +main.container .row .col h1.hero__title { + font-family: var(--fluss-font-display); + font-size: 4rem; + font-weight: 700; + letter-spacing: -0.022em; + color: var(--fluss-ink-950); +} + +/* ------------------------------------------------------------------------- + Buttons (Infima primary / outline) : global refresh + ------------------------------------------------------------------------- */ +.button.button--primary { + background: var(--fluss-blue-600); + border: none; + color: #FFFFFF; + border-radius: var(--fluss-radius-md); + font-weight: 600; + padding: 10px 18px; + box-shadow: var(--fluss-shadow-sm); + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + transform var(--fluss-motion-fast) var(--fluss-ease-out), + box-shadow var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.button.button--primary:hover { + background: var(--fluss-blue-700); + color: #FFFFFF; + transform: translateY(-1px); + box-shadow: var(--fluss-shadow-md); +} + +.button.button--outline.button--primary { + background: transparent; + border: 1px solid var(--fluss-blue-600); + color: var(--fluss-blue-700); +} + +.button.button--outline.button--primary:hover { + background: var(--fluss-blue-50); + color: var(--fluss-blue-800); +} + +/* ------------------------------------------------------------------------- + Selection + ------------------------------------------------------------------------- */ +::selection { + background: rgba(28, 80, 120, 0.18); + color: var(--fluss-ink-950); +} + +/* ------------------------------------------------------------------------- + Scrollbars (subtle, consistent across content areas) + ------------------------------------------------------------------------- */ +.theme-doc-markdown pre::-webkit-scrollbar, +.theme-doc-sidebar-container::-webkit-scrollbar, +.table-of-contents::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.theme-doc-markdown pre::-webkit-scrollbar-thumb, +.theme-doc-sidebar-container::-webkit-scrollbar-thumb, +.table-of-contents::-webkit-scrollbar-thumb { + background: var(--fluss-ink-300); + border-radius: var(--fluss-radius-pill); + border: 2px solid transparent; + background-clip: padding-box; +} + +.theme-doc-markdown pre::-webkit-scrollbar-thumb:hover, +.theme-doc-sidebar-container::-webkit-scrollbar-thumb:hover, +.table-of-contents::-webkit-scrollbar-thumb:hover { + background: var(--fluss-ink-500); + background-clip: padding-box; +} + +/* ------------------------------------------------------------------------- + Skip-to-content link (accessibility) + ------------------------------------------------------------------------- */ +.skipToContent_node_modules-\@docusaurus-theme-classic-lib-theme-SkipToContent-styles-module { + background: var(--fluss-blue-700); + color: #FFFFFF; +} + +/* ========================================================================= + ======================================================================== + PREMIUM POLISH LAYER + Targeted enhancements so docs/blog stop feeling like default Docusaurus. + ======================================================================== + ========================================================================= */ + +/* ------------------------------------------------------------------------- + Subtle page background (very faint dot grid for warmth) + ------------------------------------------------------------------------- */ +main[class*='docMainContainer'], +main[class*='blogPostPage'], +main[class*='docItemContainer'], +.docs-wrapper, +.blog-wrapper { + position: relative; + background: + radial-gradient(circle at 1px 1px, rgba(16, 40, 86, 0.04) 1px, transparent 0) 0 0 / 32px 32px, + var(--fluss-canvas); +} + +/* ------------------------------------------------------------------------- + Doc / blog-post page header band (eyebrow + title + breadcrumbs). + Scoped to single doc/post pages, NOT to the blog list (where each card + also has its own
and we don't want this treatment on cards). + ------------------------------------------------------------------------- */ + + +main[class*='docMainContainer'] article > header:first-of-type h1, +main[class*='docItemContainer'] article > header:first-of-type h1, +main[class*='blogPostPage'] article > header:first-of-type h1, +.blog-post-page-no-sidebar article > header:first-of-type h1, +.theme-doc-markdown header h1 { + font-family: var(--fluss-font-display); + font-size: clamp(2.25rem, 2.5vw + 1.5rem, 3.25rem); + font-weight: 700; + letter-spacing: -0.022em; + line-height: 1.08; + color: var(--fluss-ink-950); + margin: var(--fluss-space-3) 0 0; +} + +/* Inline ifm h1 inside the first markdown block also gets the display treatment */ +.markdown > h1:first-child { + font-family: var(--fluss-font-display); + font-size: clamp(2.25rem, 2.5vw + 1.5rem, 3.25rem); + font-weight: 700; + letter-spacing: -0.022em; + line-height: 1.08; +} + +/* ------------------------------------------------------------------------- + Markdown body : generous reading rhythm + ------------------------------------------------------------------------- */ +.theme-doc-markdown, +.markdown { + font-size: 1.0625rem; + line-height: 1.75; + color: var(--fluss-ink-700); +} + +.markdown p, +.theme-doc-markdown p { + margin-bottom: 1.25em; +} + +.markdown ul, +.markdown ol, +.theme-doc-markdown ul, +.theme-doc-markdown ol { + margin-bottom: 1.25em; +} + +.markdown li + li { + margin-top: 6px; +} + +/* Heading rhythm + display family */ +.markdown h2, +.theme-doc-markdown h2 { + font-family: var(--fluss-font-display); + font-size: 1.75rem; + font-weight: 700; + letter-spacing: -0.018em; + line-height: 1.2; + color: var(--fluss-ink-950); + margin-top: 3rem; + margin-bottom: 1rem; + position: relative; +} + +.markdown h2::before, +.theme-doc-markdown h2::before { + content: ''; + position: absolute; + left: -16px; + top: 0.55em; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--fluss-blue-600); + opacity: 0; + transition: opacity var(--fluss-motion-base) var(--fluss-ease-out); +} + +.markdown h2:hover::before, +.theme-doc-markdown h2:hover::before { + opacity: 0.8; +} + +.markdown h3, +.theme-doc-markdown h3 { + font-family: var(--fluss-font-display); + font-size: 1.375rem; + font-weight: 600; + letter-spacing: -0.014em; + color: var(--fluss-ink-950); + margin-top: 2.25rem; + margin-bottom: 0.625rem; +} + +.markdown h4, +.theme-doc-markdown h4 { + font-family: var(--fluss-font-display); + font-size: 1.0625rem; + font-weight: 600; + color: var(--fluss-ink-950); + margin-top: 1.5rem; + margin-bottom: 0.5rem; +} + +/* Anchor link reveal on heading hover */ +.markdown h2 .hash-link, +.markdown h3 .hash-link, +.markdown h4 .hash-link, +.theme-doc-markdown h2 .hash-link, +.theme-doc-markdown h3 .hash-link, +.theme-doc-markdown h4 .hash-link { + margin-left: 6px; + opacity: 0; + font-weight: 400; + transition: opacity var(--fluss-motion-base) var(--fluss-ease-out), + transform var(--fluss-motion-base) var(--fluss-ease-out); + transform: translateX(-4px); + display: inline-block; +} + +.markdown h2:hover .hash-link, +.markdown h3:hover .hash-link, +.markdown h4:hover .hash-link, +.theme-doc-markdown h2:hover .hash-link, +.theme-doc-markdown h3:hover .hash-link, +.theme-doc-markdown h4:hover .hash-link { + opacity: 1; + transform: translateX(0); +} + +/* Animated link underline in body prose. Color is explicit (not inherited + from --ifm-color-primary) so docs links read clearly as links against + the dark-gray body text. */ +.theme-doc-markdown a:not(.button):not(.card):not(.hash-link), +.markdown a:not(.button):not(.card):not(.hash-link), +.mdx-page article a:not(.button):not(.card):not(.hash-link) { + color: var(--fluss-link); + font-weight: 500; + background-image: linear-gradient(0deg, var(--fluss-link) 0, var(--fluss-link) 100%); + background-position: 0 100%; + background-repeat: no-repeat; + background-size: 100% 1px; + transition: background-size var(--fluss-motion-base) var(--fluss-ease-out), + color var(--fluss-motion-fast) var(--fluss-ease-out); + text-decoration: none; +} + +.theme-doc-markdown a:not(.button):not(.card):not(.hash-link):hover, +.markdown a:not(.button):not(.card):not(.hash-link):hover, +.mdx-page article a:not(.button):not(.card):not(.hash-link):hover { + color: var(--fluss-link-hover); + background-image: linear-gradient(0deg, var(--fluss-link-hover) 0, var(--fluss-link-hover) 100%); + background-size: 100% 2px; + text-decoration: none; +} + +/* External-link indicator (auto, before the link) */ +.theme-doc-markdown a[href^='http']:not([href*='fluss.apache.org'])::after, +.markdown a[href^='http']:not([href*='fluss.apache.org'])::after, +.mdx-page article a[href^='http']:not([href*='fluss.apache.org'])::after { + content: ''; + display: inline-block; + width: 0.7em; + height: 0.7em; + margin-left: 4px; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7M17 7H8M17 7V16'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7M17 7H8M17 7V16'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + opacity: 0.65; + vertical-align: 0.05em; +} + +/* ------------------------------------------------------------------------- + styling + ------------------------------------------------------------------------- */ +kbd { + display: inline-block; + padding: 1px 6px; + border-radius: var(--fluss-radius-sm); + background: var(--fluss-paper); + border: 1px solid var(--fluss-ink-300); + border-bottom-width: 2px; + color: var(--fluss-ink-700); + font-family: var(--fluss-font-mono); + font-size: 0.8125em; + line-height: 1.4; + vertical-align: 0.05em; + box-shadow: var(--fluss-shadow-sm); +} + +/* ------------------------------------------------------------------------- + Premium sidebar : sliding active indicator + ------------------------------------------------------------------------- */ +.theme-doc-sidebar-container { + background: + linear-gradient(180deg, transparent 0, transparent 64px, var(--fluss-canvas-soft) 64px), + var(--fluss-canvas-soft); +} + +.menu__list { + position: relative; +} + +.menu__link { + position: relative; + font-size: 0.9rem; +} + +.menu__link--active { + background: linear-gradient(90deg, var(--fluss-blue-50) 0%, transparent 100%); +} + +.menu__link--active::before { + content: ''; + position: absolute; + left: 0; + top: 6px; + bottom: 6px; + width: 3px; + border-radius: 0 var(--fluss-radius-sm) var(--fluss-radius-sm) 0; + background: linear-gradient(180deg, var(--fluss-blue-600), var(--fluss-cyan)); + box-shadow: 0 0 12px rgba(28, 80, 120, 0.4); +} + +/* Top-level items: always bold regardless of whether they are collapsible + categories or plain leaf links. Title-Case retained from `_category_.json` + (Jark feedback, PR #3226). This is the only visually heavier tier in the + sidebar; everything below it is normalized to a single uniform style. */ +.theme-doc-sidebar-menu > .menu__list-item > .menu__link, +.theme-doc-sidebar-menu > .menu__list-item > .menu__list-item-collapsible > .menu__link { + font-family: var(--fluss-font-display); + font-weight: 700; + font-size: 0.9375rem; + letter-spacing: 0; + color: var(--fluss-ink-950); + padding-top: 14px; + padding-bottom: 6px; +} + +/* Nested items at any depth: uniform non-bold styling, regardless of whether + the item is a collapsible sub-category or a leaf link. Infima's default + makes `.menu__link--sublist` bold, which created an inconsistent hierarchy + where nested sub-categories looked as heavy as top-level entries. Match a + single weight/size/padding so the only visually distinct tier is the + top-level row. */ +.theme-doc-sidebar-menu .menu__list .menu__link, +.theme-doc-sidebar-menu .menu__list .menu__list-item-collapsible > .menu__link { + font-family: inherit; + font-weight: 500; + font-size: 0.9375rem; + letter-spacing: 0; + color: var(--fluss-ink-700); + padding: 6px 10px; +} + +/* Preserve the active-state highlight for nested items (declared after the + uniform nested rule so it wins on equal specificity). */ +.theme-doc-sidebar-menu .menu__list .menu__link--active, +.theme-doc-sidebar-menu .menu__list .menu__list-item-collapsible > .menu__link--active { + color: var(--fluss-blue-700); + background: var(--fluss-blue-50); + font-weight: 600; +} + +.theme-doc-sidebar-menu > .menu__list-item:not(:first-child) { + margin-top: 4px; + border-top: 1px dashed var(--fluss-ink-100); + padding-top: 4px; +} + +/* ------------------------------------------------------------------------- + Sticky TOC card (right rail) + ------------------------------------------------------------------------- */ +.theme-doc-toc-desktop, +[class*='tableOfContents'] { + border-radius: var(--fluss-radius-lg); + background: var(--fluss-paper); + border: 1px solid var(--fluss-ink-100); + padding: var(--fluss-space-5); + box-shadow: var(--fluss-shadow-sm); + position: sticky; + top: 96px; +} + +.theme-doc-toc-desktop .table-of-contents, +[class*='tableOfContents'] .table-of-contents { + border-left: none; + padding-left: 0; + margin: 0; +} + +.theme-doc-toc-desktop::before, +[class*='tableOfContents']::before { + content: 'On this page'; + display: block; + font-family: var(--fluss-font-display); + font-weight: 700; + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fluss-ink-500); + margin-bottom: var(--fluss-space-3); + padding-bottom: var(--fluss-space-3); + border-bottom: 1px solid var(--fluss-ink-100); +} + +.table-of-contents__link { + position: relative; + padding-left: var(--fluss-space-3); + border-left: 2px solid transparent; + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.table-of-contents__link--active { + border-left-color: var(--fluss-blue-600); + background: var(--fluss-blue-50); + border-radius: 0 var(--fluss-radius-sm) var(--fluss-radius-sm) 0; +} + +/* ------------------------------------------------------------------------- + Doc footer : "edit this page" + last-updated as buttons + ------------------------------------------------------------------------- */ +.theme-doc-footer { + padding: var(--fluss-space-5); + border: 1px solid var(--fluss-ink-100); + border-radius: var(--fluss-radius-lg); + background: var(--fluss-paper); + margin-top: var(--fluss-space-12); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: var(--fluss-space-3); +} + +.theme-edit-this-page { + display: inline-flex !important; + align-items: center; + gap: 6px; + height: 36px; + padding: 0 14px; + border-radius: var(--fluss-radius-md); + background: var(--fluss-blue-50); + color: var(--fluss-blue-700) !important; + font-weight: 600; + font-size: 0.875rem; + text-decoration: none !important; + border: 1px solid transparent; + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.theme-edit-this-page:hover { + background: var(--fluss-blue-100); + border-color: var(--fluss-blue-300); +} + +/* ------------------------------------------------------------------------- + Premium code blocks + ------------------------------------------------------------------------- */ +div[class*='codeBlockContainer'] { + position: relative; + box-shadow: var(--fluss-shadow-sm); + transition: box-shadow var(--fluss-motion-base) var(--fluss-ease-out); +} + +div[class*='codeBlockContainer']:hover { + box-shadow: var(--fluss-shadow-md); +} + +/* Always-visible copy button (instead of fade-on-hover) */ +button[class*='copyButton'] { + opacity: 0.85; + transition: opacity var(--fluss-motion-fast) var(--fluss-ease-out), + color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +div[class*='codeBlockContainer'] button[class*='copyButton']:not(:hover) { + opacity: 0.85; +} + +/* Language label in the corner : synthetic via data-attr if title not set */ +div[class*='codeBlockContainer']::before { + content: attr(data-language); + position: absolute; + top: 8px; + right: 56px; + font-family: var(--fluss-font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--fluss-ink-500); + pointer-events: none; + z-index: 1; +} + +/* ------------------------------------------------------------------------- + Blog list : featured first post + editorial polish + ------------------------------------------------------------------------- */ +main[class*='blogListPage'] article:first-of-type, +.blog-list-page > article:first-of-type { + padding: var(--fluss-space-10); + background: + radial-gradient(800px 300px at 100% 0%, rgba(38, 109, 149, 0.06), transparent 60%), + radial-gradient(600px 300px at 0% 100%, rgba(28, 80, 120, 0.06), transparent 60%), + var(--fluss-paper); + border-color: var(--fluss-blue-100); +} + +main[class*='blogListPage'] article:first-of-type header h2, +.blog-list-page > article:first-of-type header h2 { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.022em; + line-height: 1.1; +} + +/* "Featured" eyebrow on the first card */ +main[class*='blogListPage'] article:first-of-type::before, +.blog-list-page > article:first-of-type::before { + content: 'Latest post'; + display: inline-block; + margin-bottom: var(--fluss-space-3); + padding: 4px 10px; + background: linear-gradient(120deg, var(--fluss-blue-600), var(--fluss-cyan)); + color: #FFFFFF; + border-radius: var(--fluss-radius-pill); + font-family: var(--fluss-font-display); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +/* Blog post titles : display font in cards */ +article[itemtype='https://schema.org/BlogPosting'] header h2, +.blog-list-page article header h2 { + font-family: var(--fluss-font-display); + letter-spacing: -0.018em; +} + +/* Blog post meta row : compact, calm */ +.blog-list-page article header > div:not([class*='blogPostTags']), +article[itemtype='https://schema.org/BlogPosting'] header > div:not([class*='blogPostTags']) { + font-size: 0.85rem; + color: var(--fluss-ink-500); +} + +/* "Read more" link with arrow */ +.button.button--secondary::after, +a[class*='readMore']::after { + content: ' →'; + display: inline-block; + margin-left: 4px; + transition: transform var(--fluss-motion-base) var(--fluss-ease-out); +} + +.button.button--secondary:hover::after, +a[class*='readMore']:hover::after { + transform: translateX(3px); +} + +/* ------------------------------------------------------------------------- + Blog post page : editorial header + ------------------------------------------------------------------------- */ +.blog-post-page-no-sidebar article > header h1, +main[class*='blogPostPage'] article > header h1 { + font-family: var(--fluss-font-display); + font-size: clamp(2.25rem, 3vw + 1rem, 3.5rem); + font-weight: 700; + letter-spacing: -0.022em; + line-height: 1.05; + margin-top: var(--fluss-space-3); + color: var(--fluss-ink-950); +} + +/* Author row : bigger, card-like */ +.blog-post-page-no-sidebar article > header > div[class*='authorRow'], +main[class*='blogPostPage'] article > header > div[class*='authorRow'] { + margin-top: var(--fluss-space-5); + padding-top: var(--fluss-space-4); + border-top: 1px solid var(--fluss-ink-100); +} + +/* ------------------------------------------------------------------------- + Pagination card refinement + ------------------------------------------------------------------------- */ +.pagination-nav { + margin-top: var(--fluss-space-12); +} + +.pagination-nav__link { + position: relative; + overflow: hidden; +} + +.pagination-nav__link::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: linear-gradient(180deg, var(--fluss-blue-600), var(--fluss-cyan)); + opacity: 0; + transition: opacity var(--fluss-motion-base) var(--fluss-ease-out); +} + +.pagination-nav__link:hover::before { + opacity: 1; +} + +.pagination-nav__link--prev .pagination-nav__sublabel::before { + content: '← '; +} + +.pagination-nav__link--next .pagination-nav__sublabel::after { + content: ' →'; +} + +/* ------------------------------------------------------------------------- + Admonition icons (color tinting) + ------------------------------------------------------------------------- */ +[class*='admonitionIcon'] svg { + filter: saturate(1.1); +} + +/* ------------------------------------------------------------------------- + Featured quote / pull-quote (use blockquote with .pull class via mdx) + Falls back gracefully for any blockquote. + ------------------------------------------------------------------------- */ +.markdown blockquote, +.theme-doc-markdown blockquote { + position: relative; + padding-left: var(--fluss-space-8); +} + +.markdown blockquote::before, +.theme-doc-markdown blockquote::before { + content: '\201C'; + position: absolute; + left: var(--fluss-space-3); + top: -0.1em; + font-family: var(--fluss-font-display); + font-size: 3rem; + font-weight: 700; + color: var(--fluss-blue-300); + line-height: 1; +} + +/* ------------------------------------------------------------------------- + Page-load fade-in for main content + ------------------------------------------------------------------------- */ +@keyframes flussFadeUp { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +main[class*='docMainContainer'] article, +main[class*='blogPostPage'] article, +main[class*='blogListPage'] article { + animation: flussFadeUp var(--fluss-motion-slow) var(--fluss-ease-out) both; +} + +@media (prefers-reduced-motion: reduce) { + main[class*='docMainContainer'] article, + main[class*='blogPostPage'] article, + main[class*='blogListPage'] article { + animation: none; + } +} + +/* ------------------------------------------------------------------------- + Increase content max-width slightly for editorial breathability + (only inside docs / blog, not on the homepage) + ------------------------------------------------------------------------- */ +@media (min-width: 997px) { + main[class*='docItemContainer'] article, + main[class*='blogPostPage'] article { + max-width: 760px; + margin-left: auto; + margin-right: auto; + } +} + +/* ------------------------------------------------------------------------- + Inline code refinements (override generic Infima) + ------------------------------------------------------------------------- */ +.markdown p code, +.markdown li code, +.theme-doc-markdown p code, +.theme-doc-markdown li code { + font-size: 0.85em; + padding: 1px 6px; + background: var(--fluss-blue-50); + border: 1px solid rgba(28, 80, 120, 0.15); + color: var(--fluss-blue-800); + border-radius: var(--fluss-radius-sm); +} + +/* ------------------------------------------------------------------------- + Tag cloud / blog tags page + ------------------------------------------------------------------------- */ +ul[class*='tag'] { + display: flex; + flex-wrap: wrap; + gap: var(--fluss-space-2); + list-style: none; + padding-left: 0; +} + +ul[class*='tag'] li { + margin: 0; + list-style: none; +} + +/* ========================================================================= + ======================================================================== + GLOBAL NAVBAR : same look on every page (homepage, docs, blog, etc.) + No transparent-on-hero variant, no homepage-frosted variant — the + navbar is rendered as a single consistent dark pane site-wide. + ======================================================================== + ========================================================================= */ + +.navbar { + background-color: var(--ifm-navbar-background-color); + box-shadow: var(--ifm-navbar-shadow); +} + +.navbar__brand, +.navbar__title, +.navbar__link { + color: #E2E8F0; +} + +.navbar__link:hover { + color: var(--fluss-blue-300); +} + +.navbar__link--active { + color: var(--fluss-blue-300); +} + +/* Hide the navbar hamburger / toggle button on desktop. Explicit product + decision: the icon to the left of the Fluss logo is not desired on any + page (homepage, docs, blog, community, learn). On docs pages the + in-doc sidebar toggle is a separate element and remains available. */ +@media (min-width: 997px) { + .navbar__toggle { + display: none !important; + } +} + +/* On mobile, keep the navbar toggle in its natural flex position at the + start of the navbar so it stays reachable at every viewport width. + Previously it was `position: absolute; right: 4rem` which made it + disappear on intermediate widths where the right-side cluster pushed + past 4rem (borzoni feedback, PR #3226). */ +@media (max-width: 996px) { + .navbar__toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + margin-right: var(--fluss-space-2); + padding: 0; + border-radius: var(--fluss-radius-md); + background: transparent; + border: 1px solid transparent; + color: #E2E8F0; + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out); + } + + .navbar__toggle:hover, + .navbar__toggle:focus-visible { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.16); + } +} + +/* ========================================================================= + ======================================================================== + COLOR MODE TOGGLE : branded sun/moon button + ======================================================================== + ========================================================================= */ + +.theme-toggle, +button[class*='colorModeToggle'], +button.clean-btn[class*='toggle'] { + border-radius: var(--fluss-radius-pill); + background: var(--fluss-ink-100); + border: 1px solid transparent; + color: var(--fluss-ink-700); + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0 6px; + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out), + transform var(--fluss-motion-fast) var(--fluss-ease-out), + color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +button[class*='colorModeToggle']:hover, +button.clean-btn[class*='toggle']:hover { + background: var(--fluss-blue-50); + border-color: var(--fluss-blue-300); + color: var(--fluss-blue-700); + transform: translateY(-1px); +} + +button[class*='colorModeToggle'] svg, +button.clean-btn[class*='toggle'] svg { + width: 18px; + height: 18px; +} + +/* The colour-mode toggle is omitted from the DOM on the landing page via + the swizzled `src/theme/ColorModeToggle/index.js` wrapper, so no CSS + hiding rule is needed here. Docs / blog / community pages keep the + toggle. */ + +/* ========================================================================= + ======================================================================== + DARK MODE : full token remap + component overrides + Triggered by [data-theme='dark'] on . + ======================================================================== + ========================================================================= */ + +[data-theme='dark'] { + /* Surfaces : deepest two palette stops give a quiet, premium dark canvas. */ + --fluss-canvas: #0A1745; + --fluss-paper: #102856; + + /* Inks (inverted) */ + --fluss-ink-950: #F8FAFC; + --fluss-ink-700: #CBD5E1; + --fluss-ink-500: #94A3B8; + --fluss-ink-300: rgba(203, 213, 225, 0.22); + --fluss-ink-100: rgba(203, 213, 225, 0.10); + + /* Blue tints recalibrated for legibility on dark, anchored on the new + navy/teal palette. */ + --fluss-blue-50: rgba(38, 109, 149, 0.14); + --fluss-blue-100: rgba(38, 109, 149, 0.22); + --fluss-blue-300: #7AAFCB; + --fluss-blue-500: #266D95; + --fluss-blue-600: #1C5078; + --fluss-blue-700: #7AAFCB; + --fluss-blue-800: #D6E4ED; + + --fluss-link: #7AAFCB; + --fluss-link-hover: #D6E4ED; + + /* Shadows on dark : softer, warmer */ + --fluss-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --fluss-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4); + --fluss-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5); + --fluss-shadow-glow: 0 0 0 1px rgba(122, 175, 203, 0.35), 0 12px 40px rgba(38, 109, 149, 0.35); + + /* Wire into Infima dark vars */ + --ifm-background-color: #0A1745; + --ifm-background-surface-color: #102856; + --ifm-color-primary: #7AAFCB; + --ifm-color-primary-dark: #266D95; + --ifm-color-primary-darker: #1C5078; + --ifm-color-primary-darkest: #194670; + --ifm-color-primary-light: #7AAFCB; + --ifm-color-primary-lighter: #B1CEDF; + --ifm-color-primary-lightest: #D6E4ED; + + --ifm-heading-color: #F8FAFC; + /* Body text bumped from #CBD5E1 (slate-300) to #F1F5F9 (slate-100) for + stronger readability in dark mode without going to pure white, which + causes halation on dark backgrounds in long-form reading. */ + --ifm-font-color-base: #F1F5F9; + + --ifm-navbar-background-color: rgba(10, 23, 69, 0.92); + --ifm-navbar-shadow: 0 1px 0 rgba(255, 255, 255, 0.06); + --ifm-navbar-link-color: #CBD5E1; + + --ifm-toc-border-color: rgba(255, 255, 255, 0.08); + --ifm-color-emphasis-200: rgba(255, 255, 255, 0.08); + --ifm-color-emphasis-300: rgba(255, 255, 255, 0.12); + --ifm-color-emphasis-600: #94A3B8; + --ifm-color-emphasis-700: #CBD5E1; + --ifm-color-emphasis-800: #F8FAFC; + + --ifm-menu-color-background-active: rgba(28, 80, 120, 0.14); + --ifm-menu-color-background-hover: rgba(28, 80, 120, 0.1); + + --docusaurus-highlighted-code-line-bg: rgba(28, 80, 120, 0.18); +} + +/* ----- Backgrounds & body ----- */ +[data-theme='dark'] body, +[data-theme='dark'] body.fluss-home { + background: var(--fluss-canvas); + background-image: none; + color: var(--fluss-ink-700); +} + +[data-theme='dark'] main[class*='docMainContainer'], +[data-theme='dark'] main[class*='blogPostPage'], +[data-theme='dark'] main[class*='docItemContainer'] { + background: + radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.04) 1px, transparent 0) 0 0 / 32px 32px, + var(--fluss-canvas); +} + +/* ----- Navbar ----- */ +[data-theme='dark'] .navbar { + border-bottom: 0; + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04), 0 2px 8px rgba(0, 0, 0, 0.3); +} + +[data-theme='dark'] .navbar__link { + color: #CBD5E1; +} + +[data-theme='dark'] .navbar__link:hover { + color: var(--fluss-blue-300); +} + +[data-theme='dark'] .navbar__link--active { + color: var(--fluss-blue-300); +} + +[data-theme='dark'] .navbar__title { + color: #F8FAFC; +} + +/* DocSearch button (dark) */ +[data-theme='dark'] .DocSearch-Button { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .DocSearch-Button:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(122, 175, 203, 0.4); +} + +[data-theme='dark'] .DocSearch-Button-Placeholder, +[data-theme='dark'] .DocSearch-Search-Icon { + color: #94A3B8; +} + +[data-theme='dark'] .DocSearch-Button-Keys { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); + color: #CBD5E1; +} + +/* GitHub icon (dark) */ +[data-theme='dark'] .header-github-link::before { + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23F8FAFC' 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.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat !important; +} + +/* ----- Sidebar (dark) ----- */ +[data-theme='dark'] .theme-doc-sidebar-container { + background: var(--fluss-canvas); + border-right-color: rgba(255, 255, 255, 0.06); +} + +[data-theme='dark'] .menu__link { + color: #CBD5E1; +} + +[data-theme='dark'] .menu__link:hover { + background: rgba(122, 175, 203, 0.12); + color: #D6E4ED; +} + +[data-theme='dark'] .menu__link--active { + background: linear-gradient(90deg, rgba(122, 175, 203, 0.18) 0%, transparent 100%); + color: #D6E4ED; +} + +[data-theme='dark'] .menu__list .menu__list { + border-left-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .theme-doc-sidebar-menu > .menu__list-item:not(:first-child) { + border-top-color: rgba(255, 255, 255, 0.06); +} + +/* ----- Breadcrumbs (dark) ----- */ +[data-theme='dark'] .breadcrumbs__link { + color: #94A3B8; +} + +[data-theme='dark'] a.breadcrumbs__link:hover { + color: #D6E4ED; + background: rgba(122, 175, 203, 0.12); +} + +[data-theme='dark'] .breadcrumbs__link:not(a) { + color: #CBD5E1; +} + +[data-theme='dark'] .breadcrumbs__item--active .breadcrumbs__link { + color: #F8FAFC; + background: rgba(255, 255, 255, 0.06); +} + +/* ----- TOC card (dark) ----- */ +[data-theme='dark'] .theme-doc-toc-desktop, +[data-theme='dark'] [class*='tableOfContents'] { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .table-of-contents__link { + color: #94A3B8; +} + +[data-theme='dark'] .table-of-contents__link:hover { + color: #D6E4ED; +} + +[data-theme='dark'] .table-of-contents__link--active { + color: #D6E4ED; + background: rgba(122, 175, 203, 0.12); +} + +/* ----- Doc body (dark) ----- */ +[data-theme='dark'] .theme-doc-markdown, +[data-theme='dark'] .markdown { + color: #F1F5F9; +} + +[data-theme='dark'] .markdown h1, +[data-theme='dark'] .markdown h2, +[data-theme='dark'] .markdown h3, +[data-theme='dark'] .markdown h4, +[data-theme='dark'] .markdown h5, +[data-theme='dark'] .markdown h6, +[data-theme='dark'] .theme-doc-markdown h1, +[data-theme='dark'] .theme-doc-markdown h2, +[data-theme='dark'] .theme-doc-markdown h3 { + color: #F8FAFC; +} + +[data-theme='dark'] .markdown b, +[data-theme='dark'] .markdown strong { + color: #F8FAFC; +} + +[data-theme='dark'] .theme-doc-markdown a:not(.button):not(.card):not(.hash-link), +[data-theme='dark'] .markdown a:not(.button):not(.card):not(.hash-link), +[data-theme='dark'] .mdx-page article a:not(.button):not(.card):not(.hash-link) { + color: #B1CEDF; + background-image: linear-gradient(0deg, #B1CEDF 0, #B1CEDF 100%); +} + +[data-theme='dark'] .markdown a.hash-link { + color: rgba(255, 255, 255, 0.18); +} + +[data-theme='dark'] .markdown a.hash-link:hover { + color: #B1CEDF; +} + +/* ----- Tables (dark) ----- */ +[data-theme='dark'] .markdown table, +[data-theme='dark'] .theme-doc-markdown table { + border-color: rgba(255, 255, 255, 0.08); + background: var(--fluss-paper); +} + +[data-theme='dark'] .markdown thead, +[data-theme='dark'] .theme-doc-markdown thead { + background: rgba(122, 175, 203, 0.1); +} + +/* The nested .markdown rule (light section) sets `table thead tr` bg to + #f6f8fa, which has higher specificity than the dark `thead` rule above + and therefore bleeds into dark mode, producing a near-white header row. + Override the `tr` explicitly for dark mode. */ +[data-theme='dark'] .markdown table thead tr, +[data-theme='dark'] .theme-doc-markdown table thead tr { + background: rgba(122, 175, 203, 0.1); +} + +[data-theme='dark'] .markdown thead th, +[data-theme='dark'] .theme-doc-markdown thead th, +[data-theme='dark'] .markdown table thead th, +[data-theme='dark'] .theme-doc-markdown table thead th { + color: #F8FAFC; + background: transparent; + border-color: rgba(255, 255, 255, 0.08); + font-weight: 600; +} + +[data-theme='dark'] .markdown tbody tr, +[data-theme='dark'] .theme-doc-markdown tbody tr { + background: transparent; +} + +[data-theme='dark'] .markdown tbody tr:nth-child(even), +[data-theme='dark'] .theme-doc-markdown tbody tr:nth-child(even) { + background: rgba(255, 255, 255, 0.03); +} + +[data-theme='dark'] .markdown table tr td, +[data-theme='dark'] .markdown table tr th { + border-color: rgba(255, 255, 255, 0.06); +} + +[data-theme='dark'] .markdown p code, +[data-theme='dark'] .markdown li code, +[data-theme='dark'] .markdown td code, +[data-theme='dark'] .theme-doc-markdown p code, +[data-theme='dark'] .theme-doc-markdown li code, +[data-theme='dark'] .theme-doc-markdown td code, +[data-theme='dark'] code { + border-color: rgba(122, 175, 203, 0.25); + color: #D6E4ED; +} + +/* ----- Blockquotes (dark) ----- */ +[data-theme='dark'] .markdown blockquote, +[data-theme='dark'] .theme-doc-markdown blockquote { + background: rgba(122, 175, 203, 0.08); + border-left-color: #7AAFCB; + color: #CBD5E1; +} + +[data-theme='dark'] .markdown blockquote::before, +[data-theme='dark'] .theme-doc-markdown blockquote::before { + color: rgba(122, 175, 203, 0.4); +} + +/* ----- Admonitions (dark) ----- */ +[data-theme='dark'] .theme-admonition, +[data-theme='dark'] .alert, +[data-theme='dark'] [class*='admonition_'] { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.1); + color: #CBD5E1; +} + +[data-theme='dark'] .alert--secondary, +[data-theme='dark'] .theme-admonition-note { + background: rgba(255, 255, 255, 0.04); + border-left-color: #94A3B8; +} + +[data-theme='dark'] .alert--info, +[data-theme='dark'] .theme-admonition-info { + background: rgba(28, 80, 120, 0.12); + border-left-color: #7AAFCB; +} + +[data-theme='dark'] .alert--success, +[data-theme='dark'] .theme-admonition-tip { + background: rgba(16, 185, 129, 0.12); + border-left-color: #34D399; +} + +[data-theme='dark'] .alert--warning, +[data-theme='dark'] .theme-admonition-warning, +[data-theme='dark'] .theme-admonition-caution { + background: rgba(245, 158, 11, 0.12); + border-left-color: #FBBF24; +} + +[data-theme='dark'] .alert--danger, +[data-theme='dark'] .theme-admonition-danger { + background: rgba(239, 68, 68, 0.12); + border-left-color: #F87171; +} + +/* ----- Code blocks (dark) ----- */ +[data-theme='dark'] div[class*='codeBlockContainer'] { + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] div[class*='codeBlockContent'] pre { + background: #102856; +} + +[data-theme='dark'] div[class*='codeBlockTitle'] { + background: rgba(122, 175, 203, 0.08); + border-bottom: 1px solid rgba(122, 175, 203, 0.18); + color: #F1F5F9; + font-weight: 500; +} + +/* The cyan-dot ::before is defined in the light section and inherits into + dark; bump its luminance so it stays visible on the darker title bg. */ +[data-theme='dark'] div[class*='codeBlockTitle']::before { + background: var(--fluss-cyan); + box-shadow: 0 0 0 2px rgba(122, 175, 203, 0.18); +} + +[data-theme='dark'] button[class*='copyButton'] { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.1); + color: #CBD5E1; +} + +[data-theme='dark'] button[class*='copyButton']:hover { + background: rgba(122, 175, 203, 0.18); + border-color: rgba(122, 175, 203, 0.45); + color: #D6E4ED; +} + +[data-theme='dark'] .theme-code-block-highlighted-line, +[data-theme='dark'] .docusaurus-highlight-code-line { + background: rgba(122, 175, 203, 0.14); + border-left-color: #7AAFCB; +} + +/* ----- Tabs (dark) ----- */ +[data-theme='dark'] .tabs { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .tabs__item { + color: #94A3B8; +} + +[data-theme='dark'] .tabs__item:hover { + background: rgba(122, 175, 203, 0.12); + color: #D6E4ED; +} + +[data-theme='dark'] .tabs__item--active { + color: #D6E4ED; + border-bottom-color: #7AAFCB; +} + +/* ----- Pagination (dark) ----- */ +[data-theme='dark'] .pagination-nav__link { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .pagination-nav__link:hover { + border-color: rgba(122, 175, 203, 0.45); +} + +[data-theme='dark'] .pagination-nav__sublabel { + color: #94A3B8; +} + +[data-theme='dark'] .pagination-nav__label { + color: #F8FAFC; +} + +/* ----- Doc footer (dark) ----- */ +[data-theme='dark'] .theme-doc-footer { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] .theme-edit-this-page { + background: rgba(122, 175, 203, 0.12); + color: #B1CEDF !important; +} + +[data-theme='dark'] .theme-edit-this-page:hover { + background: rgba(122, 175, 203, 0.2); + border-color: rgba(122, 175, 203, 0.4); +} + +[data-theme='dark'] .theme-last-updated { + color: #94A3B8; +} + +/* ----- Blog list / blog post (dark) ----- */ +[data-theme='dark'] article[itemtype='https://schema.org/BlogPosting'], +[data-theme='dark'] .blog-list-page article { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] article[itemtype='https://schema.org/BlogPosting'] header h2 a, +[data-theme='dark'] .blog-list-page article header h2 a { + color: #F8FAFC; +} + +[data-theme='dark'] article[itemtype='https://schema.org/BlogPosting'] header h2 a:hover, +[data-theme='dark'] .blog-list-page article header h2 a:hover { + color: #B1CEDF; +} + +[data-theme='dark'] main[class*='blogListPage'] article:first-of-type, +[data-theme='dark'] .blog-list-page > article:first-of-type { + background: + radial-gradient(800px 300px at 100% 0%, rgba(38, 109, 149, 0.1), transparent 60%), + radial-gradient(600px 300px at 0% 100%, rgba(28, 80, 120, 0.12), transparent 60%), + var(--fluss-paper); + border-color: rgba(122, 175, 203, 0.3); +} + +[data-theme='dark'] a.tag, +[data-theme='dark'] a[class*='tag_'] { + background: rgba(122, 175, 203, 0.14); + color: #D6E4ED; + border-color: transparent; +} + +[data-theme='dark'] a.tag:hover, +[data-theme='dark'] a[class*='tag_']:hover { + background: rgba(122, 175, 203, 0.22); + border-color: rgba(122, 175, 203, 0.4); + color: #D6E4ED; +} + +/* ----- Doc cards (dark) ----- */ +[data-theme='dark'] article.card, +[data-theme='dark'] .theme-doc-card { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] article.card:hover, +[data-theme='dark'] .theme-doc-card:hover { + border-color: rgba(122, 175, 203, 0.45); +} + +/* ----- Version banner (dark) ----- */ +[data-theme='dark'] div[class*='docVersionBanner'] { + background: rgba(122, 175, 203, 0.12); + border-color: rgba(122, 175, 203, 0.4); + color: #D6E4ED; +} + +[data-theme='dark'] div[class*='docVersionBanner'] b, +[data-theme='dark'] div[class*='docVersionBanner'] a { + color: #D6E4ED; +} + +/* ----- Dropdown menus (dark) ----- */ +[data-theme='dark'] .dropdown__menu { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.1); +} + +[data-theme='dark'] .dropdown__link { + color: #CBD5E1; +} + +[data-theme='dark'] .dropdown__link:hover, +[data-theme='dark'] .dropdown__link--active { + background: rgba(122, 175, 203, 0.14); + color: #D6E4ED; +} + +/* ----- KBD (dark) ----- */ +[data-theme='dark'] kbd { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.16); + color: #D6E4ED; +} + +/* ----- Color toggle (dark) ----- */ +[data-theme='dark'] button[class*='colorModeToggle'], +[data-theme='dark'] button.clean-btn[class*='toggle'] { + background: rgba(255, 255, 255, 0.08); + color: #D6E4ED; +} + +[data-theme='dark'] button[class*='colorModeToggle']:hover, +[data-theme='dark'] button.clean-btn[class*='toggle']:hover { + background: rgba(122, 175, 203, 0.18); + border-color: rgba(122, 175, 203, 0.4); + color: #D6E4ED; +} + +/* ----- Mobile drawer (dark) ----- */ +[data-theme='dark'] .navbar-sidebar { + background: var(--fluss-canvas); +} + +[data-theme='dark'] .navbar-sidebar__brand { + border-bottom-color: rgba(255, 255, 255, 0.06); + background: var(--fluss-canvas); +} + +/* In dark mode the drawer is dark again, so undo the light-mode inversion + on the brand wordmark and restore the light-on-dark GitHub icon. */ +[data-theme='dark'] .navbar-sidebar__brand .navbar__logo img { + filter: none; +} + +[data-theme='dark'] .navbar-sidebar__brand .navbar__title, +[data-theme='dark'] .navbar-sidebar__brand .navbar__brand { + color: #F8FAFC; +} + +[data-theme='dark'] .navbar-sidebar .navbar__link, +[data-theme='dark'] .navbar-sidebar .menu__link { + color: #CBD5E1; +} + +[data-theme='dark'] .navbar-sidebar .navbar__link:hover, +[data-theme='dark'] .navbar-sidebar .menu__link:hover { + color: var(--fluss-blue-300); +} + +[data-theme='dark'] .navbar-sidebar .header-github-link::before { + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23F8FAFC' 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.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; +} + +/* ----- Footer logo invert if needed (dark) ----- */ +[data-theme='dark'] .footer__logo { + filter: brightness(1.1); +} + +/* ----- Scrollbars (dark) ----- */ +[data-theme='dark'] .theme-doc-markdown pre::-webkit-scrollbar-thumb, +[data-theme='dark'] .theme-doc-sidebar-container::-webkit-scrollbar-thumb, +[data-theme='dark'] .table-of-contents::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.16); +} + +[data-theme='dark'] .theme-doc-markdown pre::-webkit-scrollbar-thumb:hover, +[data-theme='dark'] .theme-doc-sidebar-container::-webkit-scrollbar-thumb:hover, +[data-theme='dark'] .table-of-contents::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +/* ----- Selection (dark) ----- */ +[data-theme='dark'] ::selection { + background: rgba(122, 175, 203, 0.32); + color: #F8FAFC; +} + +/* ========================================================================= + ======================================================================== + RESPONSIVE : small viewport refinements (any device) + ======================================================================== + ========================================================================= */ + +@media (max-width: 996px) { + /* Sidebar / TOC card padding tightened */ + .theme-doc-toc-desktop, + [class*='tableOfContents'] { + padding: var(--fluss-space-4); + } + + /* Doc/blog content has full width on mobile */ + main[class*='docItemContainer'] article, + main[class*='blogPostPage'] article { + max-width: 100%; + } +} + +@media (max-width: 768px) { + /* Tighten section vertical padding on small screens */ + .theme-doc-markdown, + .markdown { + font-size: 1rem; + line-height: 1.7; + } + + /* Heading dot indicator hidden on mobile (no margin-room) */ + .markdown h2::before, + .theme-doc-markdown h2::before { + display: none; + } + + /* Pagination cards stack neatly */ + .pagination-nav { + grid-template-columns: 1fr; + gap: var(--fluss-space-3); + } + + /* Let long labels wrap & shrink instead of overflowing the card on + narrow viewports (borzoni feedback, PR #3226). */ + .pagination-nav__link { + min-width: 0; + padding: var(--fluss-space-3) var(--fluss-space-4); + } + + .pagination-nav__label { + font-size: 0.9375rem; + line-height: 1.35; + overflow-wrap: anywhere; + } + + /* Footer columns: tighter gap */ + .footer .col { + margin-bottom: var(--fluss-space-6); + } + + /* Doc footer wraps */ + .theme-doc-footer { + flex-direction: column; + align-items: flex-start; + } + + /* Smaller anchor offset */ + .theme-doc-markdown h1, + .theme-doc-markdown h2, + .theme-doc-markdown h3, + .theme-doc-markdown h4 { + scroll-margin-top: 72px; + } +} + +@media (max-width: 600px) { + /* Smaller hero internal title in case window is very narrow */ + .navbar__title { + font-size: 0.95rem; + } + + /* Hide kbd from being too cramped */ + kbd { + font-size: 0.75em; + } + + /* Tag pill smaller */ + a.tag, + a[class*='tag_'] { + height: 22px; + font-size: 0.75rem; + padding: 0 8px; + } + + /* Compare/markdown tables become horizontally scrollable comfortably */ + .markdown table, + .theme-doc-markdown table { + display: block; + overflow-x: auto; + white-space: nowrap; + } +} + +/* Very small screens : squeeze the navbar items so they don't overflow */ +@media (max-width: 480px) { + .navbar__items--right .navbar-ask-ai { + padding: 4px 8px; + font-size: 0.75rem; + } +} + +/* ========================================================================= + ======================================================================== + HOMEPAGE : dark-mode adjustments for sections that hardcode blue-950 + ======================================================================== + ========================================================================= */ + +/* In dark mode, the canvas itself is --fluss-blue-950, so sections that + intentionally use that color blend in. Lift them slightly so they read + as distinct surfaces. CSS module class names are matched by suffix. */ +[data-theme='dark'] section[class*='sectionDark'] { + background: linear-gradient(180deg, #102856 0%, #0A1745 100%); + border-top: 1px solid rgba(255, 255, 255, 0.05); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +/* Comparison table on dark mode: paper bg, refined column highlight */ +[data-theme='dark'] div[class*='compareWrap'] { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); +} + +[data-theme='dark'] table[class*='compareTable'] thead th { + background: rgba(255, 255, 255, 0.04); + color: #F8FAFC; + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +[data-theme='dark'] table[class*='compareTable'] thead th[class*='colHighlight'] { + background: linear-gradient(180deg, var(--fluss-blue-700) 0%, var(--fluss-blue-600) 100%); +} + +[data-theme='dark'] table[class*='compareTable'] tbody td { + border-top-color: rgba(255, 255, 255, 0.05); + color: #CBD5E1; +} + +[data-theme='dark'] table[class*='compareTable'] tbody td:first-child { + background: rgba(255, 255, 255, 0.03); + color: #F8FAFC; +} + +[data-theme='dark'] table[class*='compareTable'] tbody td[class*='colHighlight'] { + background: rgba(122, 175, 203, 0.1); + color: #D6E4ED; +} + +/* Code card in dark mode : slightly lifted so it stands above canvas */ +[data-theme='dark'] div[class*='codeCard'] { + background: linear-gradient(180deg, #102856 0%, #0A1745 100%); + border-color: rgba(255, 255, 255, 0.08); +} + +/* Section eyebrows / titles / leads in dark mode */ +[data-theme='dark'] span[class*='eyebrow'] { + color: #B1CEDF; +} + +[data-theme='dark'] h2[class*='sectionTitle'] { + color: #F8FAFC; +} + +[data-theme='dark'] p[class*='sectionLead'] { + color: #CBD5E1; +} + +/* Dark-mode hover/border on hover-lift cards */ +[data-theme='dark'] div[class*='card']:hover, +[data-theme='dark'] article[class*='card']:hover { + border-color: rgba(122, 175, 203, 0.45); +} + +[data-theme='dark'] div[class*='cardIcon'], +[data-theme='dark'] div[class*='iconWrap'] { + background: rgba(122, 175, 203, 0.14); + color: #D6E4ED; +} + +/* Version-badge chips on Flink section */ +[data-theme='dark'] span[class*='badge'] { + background: rgba(122, 175, 203, 0.18); + color: #D6E4ED; +} + +/* Multiple Systems Tax section in dark mode */ +[data-theme='dark'] section[class*='taxSection'] { + background: + radial-gradient(900px 500px at 50% 0%, rgba(122, 175, 203, 0.14), transparent 60%), + var(--fluss-canvas); + border-color: rgba(255, 255, 255, 0.06); +} + +[data-theme='dark'] div[class*='taxStackItem'] { + background: var(--fluss-paper); + border-color: rgba(255, 255, 255, 0.08); + border-left-color: #FB7185; +} + +[data-theme='dark'] div[class*='taxStackTitle'] { + color: #F8FAFC; +} + +[data-theme='dark'] div[class*='taxStackSub'] { + color: #94A3B8; +} + +[data-theme='dark'] span[class*='taxStackIndex'] { + color: rgba(255, 255, 255, 0.22); +} + +[data-theme='dark'] span[class*='taxLabelBefore'] { + background: rgba(244, 63, 94, 0.14); + color: #FDA4AF; + border-color: rgba(244, 63, 94, 0.3); +} + +[data-theme='dark'] p[class*='taxFootnote'] { +} + +/* ========================================================================= + Light-mode-only enhancements that need to be neutralised in dark mode + ========================================================================= */ + +/* The card resting white-tint gradient is meant for light mode. In dark + mode it would lighten the card top awkwardly — strip it. */ +[data-theme='dark'] section[class*='introduce'] div[class*='card'], +[data-theme='dark'] section[class*='features'] article[class*='card'] { + background: var(--fluss-paper); +} + +/* Section background washes use white-leaning gradients in light mode. + In dark mode the canvas already provides the deep blue, but we want + subtle blue-tinted radial highlights on plain sections instead. */ +[data-theme='dark'] section[class*='introduce'], +[data-theme='dark'] section[class*='features'] { + background: + radial-gradient(1100px 480px at 100% 0%, rgba(122, 175, 203, 0.1), transparent 60%), + radial-gradient(900px 460px at 0% 100%, rgba(122, 175, 203, 0.08), transparent 60%), + var(--fluss-canvas); + border-top-color: rgba(255, 255, 255, 0.06); +} + +/* Generic .section radial wash on light mode — recolour for dark */ +[data-theme='dark'] section[class*='section'][class*='index_section'], +[data-theme='dark'] section[class*='index_section'] { + background: + radial-gradient(1100px 480px at 80% 0%, rgba(122, 175, 203, 0.08), transparent 60%), + radial-gradient(900px 460px at 0% 100%, rgba(122, 175, 203, 0.08), transparent 60%), + var(--fluss-canvas); +} + +/* Eyebrow chip override for dark mode — the auto-mapped background is + already a translucent blue, but the border needs darkening too. */ +[data-theme='dark'] span[class*='eyebrow'] { + border-color: rgba(122, 175, 203, 0.28); +} + +/* Card icon gradient is meant for light. Re-tone for dark. */ +[data-theme='dark'] div[class*='cardIcon'], +[data-theme='dark'] div[class*='iconWrap'] { + background: linear-gradient(135deg, rgba(122, 175, 203, 0.18) 0%, rgba(38, 109, 149, 0.18) 100%); + box-shadow: inset 0 0 0 1px rgba(122, 175, 203, 0.22); + color: #D6E4ED; +} + +/* Comparison-table accent bar visible against dark surface */ +[data-theme='dark'] div[class*='compareWrap']::before { + background: linear-gradient(90deg, var(--fluss-blue-500), var(--fluss-cyan)); +} + +[data-theme='dark'] table[class*='compareTable'] tbody tr:hover td { + background-color: rgba(122, 175, 203, 0.08); +} + +[data-theme='dark'] table[class*='compareTable'] tbody tr:hover td:first-child { + background-color: rgba(122, 175, 203, 0.12); +} + +[data-theme='dark'] table[class*='compareTable'] tbody tr:hover td[class*='colHighlight'] { + background-color: rgba(122, 175, 203, 0.2); +} diff --git a/website/src/pages/compare/kafka.mdx b/website/src/pages/compare/kafka.mdx new file mode 100644 index 0000000000..93f32216a1 --- /dev/null +++ b/website/src/pages/compare/kafka.mdx @@ -0,0 +1,108 @@ +--- +title: Apache Fluss vs Apache Kafka +description: How Apache Fluss compares to Apache Kafka. When each is the right tool for your real-time analytics, AI/ML, and lakehouse stack. +--- + +# Apache Fluss vs Apache Kafka + +Apache Kafka and Apache Fluss occupy different layers of the real-time data +stack. Kafka is a **streaming transport**: a durable, distributed commit log +built to move events between systems. Fluss is **streaming storage**: a +columnar table substrate built to serve large-scale stream processing, +real-time analytics, AI/ML, and lakehouse queries from the same data, in +seconds. + +This page covers when each tool is the right pick and how they differ. + +## TL;DR + +- **Use Kafka** when your primary need is durable event transport between + services or systems: pub/sub, log ingestion, microservice fan-out, + cross-language messaging. +- **Use Fluss** when your primary need is large-scale stream processing with + Apache Flink, real-time analytics, or AI/ML pipelines. Fluss is one shared + streaming storage substrate for all of them, so analytics jobs and AI/ML + workloads read from the same data without copies and without separate + feature, context, or analytical stores. + +## The real distinction + +Kafka treats data as **rows in an append-only log** addressable by partition +and offset. That model is excellent for transport. Every consumer gets a +strictly ordered, replayable stream, but it pushes the cost of analytical +access (filtering, joining, aggregating, deduplicating) onto the consuming +application. State for those operations ends up on the consumer side, typically +in RocksDB inside Flink, which makes recovery slow and scaling state-bound. + +Fluss treats data as **tables**. Two kinds: **Log Tables** for append-only +streams, and **Primary Key Tables** that support native upserts, partial +updates, and deletes, with a column-oriented Arrow log and an LSM-based KV +index sitting behind the same table. Reads are server-side: column projection, +predicate pushdown, and partition pruning happen on the TabletServer before +bytes hit the wire. PK lookups are a first-class operation. And cold data tiers +automatically into Iceberg, Paimon, or Lance in their native open format, +queryable as the same logical table from Spark, Trino, StarRocks, or DuckDB. + +## When Kafka is the right tool + +Pick Kafka when these are your dominant needs: + +- **Event-driven systems.** Services publish events; many services subscribe. + Kafka's at-least-once / exactly-once delivery, consumer groups, and rich + ecosystem of clients in every language make this its strongest fit. +- **Log ingestion and edge transport.** Application logs, click streams, + device telemetry. Funnel them through Kafka before they fan out to + downstream stores. +- **Microservice pub/sub.** Asynchronous decoupling between services. +- **Cross-system, cross-language messaging.** Kafka's broad client support + remains unmatched. + +## When Fluss is the right tool + +Pick Fluss when these are your dominant needs: + +- **Large-scale stream processing with Apache Flink.** Stateless compute, + with join and aggregation state externalised onto Fluss via Delta Joins + and the Aggregation Merge Engine. Recovery drops from minutes to seconds + and compute scales independently of state size. +- **Real-time analytics on wide tables.** Server-side column projection, + predicate pushdown, and partition pruning compound into order-of-magnitude + I/O and network savings. A query reading 10 columns out of 200 transfers + about 5% of the bytes a row-log-based pipeline would. +- **AI / ML on streaming data.** Row, columnar, and vector formats sit on the + same substrate. Online feature serving, RAG-ready semantic context, and + structured analytics collapse into one PK Table accessed through different + views. No separate feature store, no separate context store. +- **Dimension joins and stream enrichment.** PK lookups are native and + sub-millisecond. Flink Lookup Joins against Fluss PK Tables consolidate + the online KV store (Redis, HBase, Cassandra) into the same substrate that + holds the streaming log, so there is one source of truth for both the + serving lookup and the changelog instead of a separate cache in front of + the pipeline. +- **CDC-heavy pipelines.** Primary Key Tables handle upserts, partial updates, + and deletes natively, and emit a `$changelog` virtual table that is + replayable by design. No external Schema Registry and no Connect/Debezium + layer needed for CDC patterns within Fluss. +- **Real-time lakehouse.** Fluss is the hot tier; Iceberg or Paimon is the + cold tier. They share a schema and are queryable as one logical table + through Union Read, so streaming jobs and historical queries hit the same + source of truth with sub-second freshness. Lance is supported as a tiering + target for AI / vector workloads (Union Read on Lance is on the roadmap). + +## Side-by-side + +| Dimension | Apache Kafka | Apache Fluss | +| --- | --- | --- | +| **Positioning** | Distributed event streaming platform / durable commit log | Streaming storage for real-time analytics, AI/ML, and the lakehouse | +| **Storage model** | Append-only row log | Columnar Arrow log & KV index; tiers to Paimon · Iceberg · Lance | +| **Logical unit** | Topic (log only) | Log Tables & Primary Key Tables with native upserts, partial updates, deletes | +| **Metadata plane** | KRaft controllers · keyed topic partitions | CoordinatorServer & TabletServers · buckets & first-class partitioned tables | +| **Schema · CDC** | External Schema Registry; CDC via Connect / Debezium | First-class schemas with evolution; native `$changelog` · `$binlog` virtual tables | +| **Read path** | No server-side pruning; no native PK lookup | Zero-copy column · partition · predicate pushdown; PK lookup via LSM | +| **State externalisation** (with Flink) | App holds join & aggregation state in RocksDB | Delta Joins & Aggregation Merge Engine externalise state to Fluss | +| **Lakehouse integration** | External (via Connect sinks) | Native (shared schema and Union Read across Iceberg & Paimon; Lance for AI / vector tiering) | +| **Engines that read** (the storage layer) | Kafka clients only | Flink · Spark · DuckDB· **_planned:** Trino · StarRocks | +| **Strong fit** | Event-driven systems · log ingestion · microservice pub/sub · cross-language transport | Large-scale Flink stream processing · real-time analytics · AI/ML · streaming lakehouse · dimension joins · CDC | + +Ready to try Fluss? [Get started with the Flink quickstart](/docs/quickstart/flink), +or read the [architecture overview](/docs/concepts/architecture). diff --git a/website/src/pages/index.module.css b/website/src/pages/index.module.css index 7ea638a7e2..ef86d4984d 100644 --- a/website/src/pages/index.module.css +++ b/website/src/pages/index.module.css @@ -16,87 +16,1044 @@ */ /** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. + * Homepage CSS modules. Marketing-only styles for the redesigned home page. */ +.homepageWrapper { + background: transparent; +} + +/* Selected text on the homepage's dark sections needs a light foreground + so it stays readable. The global ::selection in custom.css uses a near-black + text color which disappears against the dark hero / community cards. */ +.homepageWrapper ::selection { + background: rgba(122, 175, 203, 0.45); + color: #FFFFFF; +} + +.homepageWrapper ::-moz-selection { + background: rgba(122, 175, 203, 0.45); + color: #FFFFFF; +} + +/* ========================================================================= + Hero + ========================================================================= */ .heroBanner { - padding: 5rem 0 11rem 0; - text-align: center; position: relative; overflow: hidden; - margin-left: -1px; margin-top: -60px; - padding-top: calc(5rem + 60px); + padding: calc(var(--fluss-space-32) + 60px) 0 var(--fluss-space-24); + /* Hero gradient pinned to the dark-mode rendering for BOTH themes. + * The bottom stop is the dark-mode --fluss-blue-800 override value + * (#D6E4ED, light blue) so the gradient fades from deep blue to light + * blue identically in light and dark — composited with the cyan + + * brand-blue radial overlays above for the layered hero look. */ + background: + radial-gradient(1200px 600px at 80% 0%, rgba(38, 109, 149, 0.18), transparent 60%), + radial-gradient(900px 500px at 10% 100%, rgba(38, 109, 149, 0.25), transparent 60%), + linear-gradient(180deg, #0A1745 0%, #102856 60%, #D6E4ED 100%); + color: #FFFFFF; +} - @media screen and (min-width: 997px) { - background-image: url("@site/static/img/new_banner.png"); - background-size: cover; - background-position: center center; - background-repeat: no-repeat; +.heroBanner::before { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); + background-size: 56px 56px; + mask-image: radial-gradient(ellipse at center, black 50%, transparent 80%); + -webkit-mask-image: radial-gradient(ellipse at center, black 50%, transparent 80%); + pointer-events: none; +} + +/* Hero gets a wider container with much tighter side padding than the rest + of the homepage, so the side-by-side text + diagram can use almost the + full viewport. Other sections keep the standard 1240 / 24px container. + + The side padding ramps up at smaller viewports so the hero content does + not run flush to the screen edge on typical laptop sizes: + ≥ 1500px : 0 (max diagram space, container is centred with margin) + ≤ 1500px : 12px + ≤ 1280px : 20px + ≤ 996px : 24px (matches the standard container padding under the + existing mobile stack media query) */ +.heroBanner .container { + max-width: 1560px; + padding-left: 0; + padding-right: 0; +} + +@media (max-width: 1500px) { + .heroBanner .container { + padding-left: var(--fluss-space-3); + padding-right: var(--fluss-space-3); } +} - @media screen and (max-width: 996px) { - background: var(--ifm-color-primary-darkest); +@media (max-width: 1280px) { + .heroBanner .container { + padding-left: var(--fluss-space-5); + padding-right: var(--fluss-space-5); } +} + +.heroInner { + position: relative; + display: grid; + /* Text column on the left, code panel on the right. Equal split so + the title block and the SQL editor share the hero horizontally. */ + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--fluss-space-12); + align-items: center; +} + +.heroEyebrow { + display: inline-flex; + align-items: center; + gap: var(--fluss-space-2); + padding: 6px 12px; + border-radius: var(--fluss-radius-pill); + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.16); + color: var(--fluss-blue-100); + font-size: 0.8125rem; + font-weight: 500; + letter-spacing: 0.04em; + text-transform: uppercase; + backdrop-filter: blur(8px); + margin-bottom: var(--fluss-space-6); +} + +.heroEyebrow .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--fluss-cyan); + box-shadow: 0 0 0 4px rgba(38, 109, 149, 0.2); +} + +.heroTitle { + font-family: var(--fluss-font-display); + font-size: clamp(2.5rem, 5vw + 1rem, 4.25rem); + font-weight: 700; + line-height: 1.05; + letter-spacing: -0.022em; + text-wrap: balance; + margin: 0 0 var(--fluss-space-6); + color: #FFFFFF; +} + +.heroTitle .accent { + /* Hero accent gradient — traverses cyan → blue → violet → white so it + * reads obviously as a gradient on the deep-blue feature surface, + * not as a near-monochrome glow. The four-stop curve plants violet + * as a mid-anchor, which gives the cyan→white sweep visible motion. */ + background: linear-gradient( + 120deg, + var(--fluss-cyan) 0%, + var(--fluss-blue-300) 35%, + #C4B5FD 65%, + #FFFFFF 100% + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.heroSubtitle { + font-size: 1.0625rem; + line-height: 1.65; + color: rgba(219, 234, 254, 0.88); + max-width: 580px; + margin: 0 0 var(--fluss-space-8); + text-wrap: pretty; +} + +.heroCtas { + display: flex; + flex-wrap: wrap; + gap: var(--fluss-space-3); + align-items: center; +} + +/* Primary CTA button — pinned to the dark-mode rendering for both themes. + * #266D95 is the dark-mode --fluss-blue-600 override; #7AAFCB is the + * dark-mode --fluss-blue-500 override. */ +.btnPrimary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 48px; + padding: 0 22px; + border-radius: var(--fluss-radius-md); + background: #266D95; + color: #FFFFFF; + font-weight: 600; + font-size: 0.9375rem; + letter-spacing: 0.005em; + box-shadow: var(--fluss-shadow-glow); + transition: transform var(--fluss-motion-fast) var(--fluss-ease-out), + box-shadow var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.btnPrimary:hover { + transform: translateY(-1px); + background: #7AAFCB; + color: #FFFFFF; + text-decoration: none; + box-shadow: 0 0 0 1px rgba(38, 109, 149, 0.35), 0 16px 48px rgba(28, 80, 120, 0.32); +} + +.btnSecondary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 48px; + padding: 0 18px; + border-radius: var(--fluss-radius-md); + background: rgba(255, 255, 255, 0.06); + color: #FFFFFF; + font-weight: 600; + font-size: 0.9375rem; + border: 1px solid rgba(255, 255, 255, 0.18); + backdrop-filter: blur(10px); + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out), + border-color var(--fluss-motion-fast) var(--fluss-ease-out), + transform var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.btnSecondary:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.32); + color: #FFFFFF; + text-decoration: none; + transform: translateY(-1px); +} + +.btnIcon { + width: 18px; + height: 18px; +} + +/* Right column of the hero: holds the tabbed code panel. */ +.heroCodeColumn { + width: 100%; + min-width: 0; +} + +/* Combined chrome row: traffic-light dots on the left, engine tabs on the + right. Single header strip with the dots and the engine tabs together. */ +.heroCodeHeader { + display: flex; + align-items: center; + gap: var(--fluss-space-3); + padding: 8px 12px; + background: rgba(255, 255, 255, 0.02); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.heroCodeDots { + display: inline-flex; + gap: 6px; + flex-shrink: 0; +} + +.heroCodeDots span { + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); +} + +.heroCodeDots span:nth-child(1) { background: #FF5F57; } +.heroCodeDots span:nth-child(2) { background: #FEBC2E; } +.heroCodeDots span:nth-child(3) { background: #28C840; } + +/* Engine tabs (Flink SQL / Spark SQL) inside the hero code card. */ +.heroCodeTabs { + display: inline-flex; + gap: 2px; +} + +.heroCodeTab { + appearance: none; + background: transparent; + border: 0; + color: rgba(219, 234, 254, 0.55); + font-family: var(--fluss-font-mono); + font-size: 0.8125rem; + font-weight: 600; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + transition: color var(--fluss-motion-fast) var(--fluss-ease-out), + background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} +.heroCodeTab:hover { + color: rgba(219, 234, 254, 0.9); } -@media screen and (max-width: 996px) { +.heroCodeTabActive, +.heroCodeTabActive:hover { + color: #FFFFFF; + background: rgba(38, 109, 149, 0.15); +} + +/* Trust strip below hero */ +.trustStrip { + position: relative; + margin-top: var(--fluss-space-16); + padding: var(--fluss-space-5) var(--fluss-space-6); + border-radius: var(--fluss-radius-lg); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + flex-wrap: wrap; + gap: var(--fluss-space-6); + align-items: center; + justify-content: space-between; + color: rgba(219, 234, 254, 0.85); + font-size: 0.875rem; + backdrop-filter: blur(10px); +} + +.trustItem { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.trustDivider { + width: 1px; + height: 18px; + background: rgba(255, 255, 255, 0.12); +} + +@media (max-width: 996px) { .heroBanner { - padding: 2rem; + padding-top: calc(var(--fluss-space-20) + 60px); + padding-bottom: var(--fluss-space-20); + } + .heroBanner .container { + padding-left: var(--fluss-container-pad); + padding-right: var(--fluss-container-pad); + } + .heroInner { + grid-template-columns: 1fr; + gap: var(--fluss-space-12); + } + .trustStrip { + flex-direction: column; + align-items: flex-start; + gap: var(--fluss-space-3); + } + .trustDivider { + display: none; + } +} + +/* ========================================================================= + Generic section primitives + ========================================================================= */ +.section { + position: relative; + padding: var(--fluss-space-24) 0; + /* Always-dark — pinned to deep-blue gradient for both modes. + * Cyan + brand-blue radial overlays sit on a dark base. */ + background: + radial-gradient(1100px 520px at 85% -10%, rgba(38, 109, 149, 0.14), transparent 60%), + radial-gradient(900px 460px at -5% 110%, rgba(28, 80, 120, 0.18), transparent 60%), + linear-gradient(180deg, #0A1745 0%, #102856 100%); + color: rgba(219, 234, 254, 0.88); +} + +.sectionTight { + padding: var(--fluss-space-16) 0; +} + +.sectionDark { + background: var(--fluss-blue-950); + color: rgba(219, 234, 254, 0.88); +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px 14px; + border-radius: var(--fluss-radius-pill); + background: linear-gradient(120deg, rgba(38, 109, 149, 0.18) 0%, rgba(28, 80, 120, 0.14) 100%); + border: 1px solid rgba(122, 175, 203, 0.32); + color: #B1CEDF; + font-family: var(--fluss-font-display); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: var(--fluss-space-4); + box-shadow: 0 2px 8px rgba(38, 109, 149, 0.12); +} + +.eyebrow::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: linear-gradient(120deg, var(--fluss-blue-600), var(--fluss-cyan)); +} + +.sectionDark .eyebrow { + background: rgba(38, 109, 149, 0.1); + border-color: rgba(38, 109, 149, 0.28); + color: var(--fluss-cyan); +} + +.sectionTitle { + font-family: var(--fluss-font-display); + font-size: clamp(2rem, 3vw + 1rem, 3rem); + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + text-wrap: balance; + margin: 0 0 var(--fluss-space-4); + color: #FFFFFF; +} + +.sectionDark .sectionTitle { + color: #FFFFFF; +} + +.sectionLead { + font-size: 1.125rem; + line-height: 1.65; + color: rgba(219, 234, 254, 0.78); + max-width: 720px; + margin: 0 0 var(--fluss-space-12); + text-wrap: pretty; +} + +.sectionDark .sectionLead { + color: rgba(219, 234, 254, 0.78); +} + +.sectionHeader { + text-align: left; + margin-bottom: var(--fluss-space-12); +} + +.sectionHeaderCenter { + text-align: center; + max-width: 760px; + margin-left: auto; + margin-right: auto; +} + +.sectionHeaderCenter .sectionLead { + margin-left: auto; + margin-right: auto; +} + +/* ========================================================================= + Comparison matrix + ========================================================================= */ +.compareWrap { + position: relative; + border-radius: var(--fluss-radius-xl); + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.08); + background: + radial-gradient(800px 240px at 100% 0%, rgba(38, 109, 149, 0.12), transparent 60%), + #102856; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55), 0 6px 16px rgba(0, 0, 0, 0.32); +} + +.compareWrap::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 3px; + background: linear-gradient(90deg, var(--fluss-blue-600), var(--fluss-cyan)); + z-index: 1; +} + +.compareTable { + width: 100%; + border-collapse: collapse; + font-size: 0.9375rem; +} + +.compareTable thead th { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.02) 100%); + color: rgba(219, 234, 254, 0.92); + font-weight: 600; + text-align: left; + padding: var(--fluss-space-4) var(--fluss-space-5); + border-bottom: 1px solid rgba(255, 255, 255, 0.10); + white-space: nowrap; +} + +.compareTable thead th.colHighlight { + background: linear-gradient(180deg, #194670 0%, #1C5078 100%); + color: #FFFFFF; +} + +.compareTable tbody tr { + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.compareTable tbody tr:hover td { + background-color: rgba(255, 255, 255, 0.03); +} + +.compareTable tbody tr:hover td:first-child { + background-color: rgba(255, 255, 255, 0.05); +} + +.compareTable tbody tr:hover td.colHighlight { + background-color: rgba(122, 175, 203, 0.14); +} + +.compareTable tbody td { + padding: var(--fluss-space-4) var(--fluss-space-5); + border-top: 1px solid rgba(255, 255, 255, 0.06); + vertical-align: top; + color: #FFFFFF; + transition: background-color var(--fluss-motion-fast) var(--fluss-ease-out); +} + +.compareTable tbody td:first-child { + font-weight: 600; + color: #FFFFFF; + background: linear-gradient(90deg, rgba(122, 175, 203, 0.08) 0%, transparent 100%); + width: 28%; +} + +.compareTable tbody td.colHighlight { + background: linear-gradient(180deg, rgba(122, 175, 203, 0.10) 0%, rgba(38, 109, 149, 0.10) 100%); + color: #FFFFFF; + font-weight: 500; + box-shadow: inset 1px 0 0 rgba(122, 175, 203, 0.20), inset -1px 0 0 rgba(122, 175, 203, 0.20); +} + +@media (max-width: 800px) { + .compareWrap { + overflow-x: auto; } + .compareTable { + min-width: 720px; + } +} - .heroBanner :global(.hero__title) { - font-size: 2.5rem; +/* ========================================================================= + Flink integration band + ========================================================================= */ +.flinkBand { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); + gap: var(--fluss-space-12); + align-items: center; +} + +@media (max-width: 996px) { + .flinkBand { + grid-template-columns: 1fr; } +} + +.versionBadges { + display: flex; + flex-wrap: wrap; + gap: var(--fluss-space-2); + margin-top: var(--fluss-space-6); +} - .heroBanner :global(.hero__subtitle) { - font-size: 1.2rem; +.badge { + display: inline-flex; + align-items: center; + height: 28px; + padding: 0 14px; + border-radius: var(--fluss-radius-pill); + background: linear-gradient(120deg, #FFFFFF 0%, var(--fluss-blue-100) 100%); + color: var(--fluss-blue-800); + border: 1px solid rgba(28, 80, 120, 0.20); + font-size: 0.8125rem; + font-weight: 600; + letter-spacing: 0.01em; + box-shadow: 0 1px 2px rgba(16, 40, 86, 0.06); +} + +/* ========================================================================= + Code block (homepage snippet) + ========================================================================= */ +.codeCard { + border-radius: var(--fluss-radius-lg); + background: var(--fluss-blue-950); + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: var(--fluss-shadow-lg); + overflow: hidden; +} + +.codeBody { + padding: var(--fluss-space-5) var(--fluss-space-6); + color: rgba(219, 234, 254, 0.92); + font-family: var(--fluss-font-mono); + font-size: 0.875rem; + line-height: 1.7; + overflow-x: auto; + margin: 0; + background: transparent; +} + +/* ========================================================================= + Stats / community + ========================================================================= */ +.statsRow { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--fluss-space-4); + margin-top: var(--fluss-space-12); +} + +@media (max-width: 800px) { + .statsRow { + grid-template-columns: 1fr; } } -.heroBanner :global(.hero__title) { - font-family: 'Roboto', sans-serif; - font-size: 4rem; +.statCard { + padding: var(--fluss-space-6); + border-radius: var(--fluss-radius-lg); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + text-align: left; +} + +.statValue { + font-family: var(--fluss-font-display); + font-size: 2.25rem; font-weight: 700; + color: #FFFFFF; letter-spacing: -0.02em; - line-height: 1.1; - text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + margin-bottom: var(--fluss-space-1); + line-height: 1; } -.heroBanner :global(.hero__subtitle) { - font-family: 'Roboto', sans-serif; - font-size: 1.5rem; - font-weight: 400; - letter-spacing: 0.01em; - text-shadow: 0 1px 8px rgba(0, 0, 0, 0.2); - margin-top: 1rem; - opacity: 0.9; +.statLabel { + font-size: 0.875rem; + color: rgba(219, 234, 254, 0.7); } -.buttons { +.communityGrid { + margin-top: var(--fluss-space-12); + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--fluss-space-4); +} + +@media (max-width: 800px) { + .communityGrid { + grid-template-columns: 1fr; + } +} + +.communityCard { + display: flex; + flex-direction: column; + padding: var(--fluss-space-6); + border-radius: var(--fluss-radius-lg); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + color: rgba(219, 234, 254, 0.88); + text-decoration: none; + transition: transform var(--fluss-motion-base) var(--fluss-ease-out), + background-color var(--fluss-motion-base) var(--fluss-ease-out), + border-color var(--fluss-motion-base) var(--fluss-ease-out); +} + +.communityCard:hover { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.07); + border-color: rgba(38, 109, 149, 0.4); + color: #FFFFFF; + text-decoration: none; +} + +.communityTitle { + font-family: var(--fluss-font-display); + font-size: 1.125rem; + font-weight: 600; + color: #FFFFFF; + margin-bottom: var(--fluss-space-2); +} + +.communityArrow { + margin-top: auto; + padding-top: var(--fluss-space-4); + color: var(--fluss-blue-300); + font-weight: 600; + font-size: 0.875rem; +} + +/* ========================================================================= + Container helper (matches Infima but tunable) + ========================================================================= */ +.container { + max-width: var(--fluss-container-max); + margin: 0 auto; + padding: 0 var(--fluss-container-pad); +} + +/* ========================================================================= + Multiple Systems Tax (Before / After) + ========================================================================= */ +.taxSection { + padding: var(--fluss-space-24) 0; + /* Always-dark — pinned to deep-blue gradient for both modes. */ + background: + radial-gradient(900px 500px at 50% -10%, rgba(28, 80, 120, 0.20), transparent 60%), + radial-gradient(700px 360px at 0% 100%, rgba(244, 63, 94, 0.10), transparent 60%), + radial-gradient(700px 360px at 100% 100%, rgba(38, 109, 149, 0.14), transparent 60%), + linear-gradient(180deg, #0A1745 0%, #102856 100%); + border-top: 1px solid rgba(255, 255, 255, 0.06); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.taxGrid { + /* The grid declares THREE shared rows (auto / 1fr / auto) for + label / main-box / footnote. Each .taxColumn below uses + `grid-template-rows: subgrid` so its inner rows align to these + shared tracks. Net effect: the main-box row resolves to the + same height in both columns regardless of how each column's + footnote wraps, so the Before stack and the After card start + and end at the exact same Y. */ + display: grid; + grid-template-columns: 1fr 60px 1fr; + grid-template-rows: auto 1fr auto; + column-gap: var(--fluss-space-6); + row-gap: var(--fluss-space-4); + align-items: stretch; +} + +@media (max-width: 996px) { + .taxGrid { + grid-template-columns: 1fr; + /* Drop the shared 3-row template on mobile — each column stacks + independently and the boxes naturally take their content + height. */ + grid-template-rows: none; + column-gap: 0; + row-gap: var(--fluss-space-12); + } +} + +.taxColumn { + /* Subgrid: inherits the parent .taxGrid's three rows so label, + main-box, and footnote in BOTH columns line up to the same Y + coordinates. */ + display: grid; + grid-template-rows: subgrid; + grid-row: 1 / -1; + min-width: 0; +} + +@media (max-width: 996px) { + .taxColumn { + /* Fall back to a regular 3-row grid on mobile (subgrid only + makes sense when the parent has multi-column tracks to + share). */ + grid-template-rows: auto 1fr auto; + grid-row: auto; + gap: var(--fluss-space-4); + } +} + +.taxLabel { + display: inline-flex; + align-items: center; + align-self: flex-start; + gap: 8px; + padding: 6px 14px; + border-radius: var(--fluss-radius-pill); + font-family: var(--fluss-font-display); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.taxLabelBefore { + background: rgba(244, 63, 94, 0.16); + color: #FDA4AF; + border: 1px solid rgba(244, 63, 94, 0.32); +} + +.taxLabelAfter { + background: linear-gradient(120deg, var(--fluss-blue-600), var(--fluss-cyan)); + color: #FFFFFF; + border: 1px solid transparent; +} + +/* Before stack */ +.taxStack { + display: flex; + flex-direction: column; + gap: var(--fluss-space-2); + /* Pin to the full height of the grid row so the Before stack ends + at exactly the same Y as the After card. `align-self: stretch` + is the default for grid items, but `height: 100%` makes the + intent explicit and works even when inner content has its own + intrinsic block size. `min-height: 0` lets the flex children + shrink/grow inside without being held up by their content + min-size. */ + height: 100%; + min-height: 0; + align-self: stretch; +} + +.taxStackItem { + position: relative; + display: grid; + grid-template-columns: 32px 1fr; + gap: var(--fluss-space-3); + align-items: flex-start; + padding: var(--fluss-space-4) var(--fluss-space-5); + /* Each Before item shares the stack's grown height equally so the + five cards expand together rather than leaving empty space at + the bottom of the column. */ + flex: 1; + border-radius: var(--fluss-radius-md); + background: + linear-gradient(90deg, rgba(244, 63, 94, 0.10) 0%, rgba(255, 255, 255, 0.04) 40%); + border: 1px solid rgba(244, 63, 94, 0.22); + border-left: 3px solid #FB7185; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.25); + transition: transform var(--fluss-motion-base) var(--fluss-ease-out), + border-color var(--fluss-motion-base) var(--fluss-ease-out), + box-shadow var(--fluss-motion-base) var(--fluss-ease-out); +} + +.taxStackItem:hover { + transform: translateX(3px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); + border-color: rgba(244, 63, 94, 0.42); +} + +.taxStackIndex { + font-family: var(--fluss-font-mono); + font-size: 0.75rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.42); + line-height: 1.5; + padding-top: 2px; +} + +.taxStackTitle { + font-family: var(--fluss-font-display); + font-size: 0.9375rem; + font-weight: 600; + color: #FFFFFF; + line-height: 1.3; +} + +.taxStackSub { + font-size: 0.8125rem; + line-height: 1.45; + color: rgba(219, 234, 254, 0.78); + margin-top: 2px; +} + +/* Arrow column */ +.taxArrow { + /* Parent .taxGrid now has three rows; the arrow spans all of them + so it stays vertically centred between the Before stack and the + After card. */ + grid-row: 1 / -1; display: flex; align-items: center; justify-content: center; - flex-wrap: wrap; - gap: 20px; - margin-top: 2rem; + height: 100%; + min-height: 200px; +} + +@media (max-width: 996px) { + .taxArrow { + /* On mobile the parent grid collapses to a single column with + no row template, so the spanning rule no longer applies. */ + grid-row: auto; + } +} + +.taxArrow svg { + width: 60px; + height: 24px; + filter: drop-shadow(0 2px 12px rgba(38, 109, 149, 0.4)); +} + +@media (max-width: 996px) { + .taxArrow { + min-height: 0; + } + .taxArrow svg { + width: 40px; + transform: rotate(90deg); + } +} + +/* After card */ +.taxAfterCard { + padding: var(--fluss-space-6); + border-radius: var(--fluss-radius-lg); + background: + radial-gradient(600px 300px at 100% 0%, rgba(38, 109, 149, 0.12), transparent 60%), + linear-gradient(180deg, var(--fluss-blue-700) 0%, var(--fluss-blue-800) 100%); + color: #FFFFFF; + box-shadow: var(--fluss-shadow-glow); + border: 1px solid rgba(255, 255, 255, 0.1); + /* Pin to the full height of the grid row so the After card ends at + exactly the same Y as the Before stack. See .taxStack above for + the same rationale. */ + height: 100%; + min-height: 0; + align-self: stretch; +} + +.taxAfterHeader { + margin-bottom: var(--fluss-space-5); + padding-bottom: var(--fluss-space-4); + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.taxAfterTitle { + font-family: var(--fluss-font-display); + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.018em; + color: #FFFFFF; + margin-bottom: 6px; +} + +.taxAfterSub { + font-size: 0.9375rem; + line-height: 1.55; + color: rgba(219, 234, 254, 0.85); } -.buttonWidth { - width: 200px; +.taxAfterList { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--fluss-space-4); } -.buttonWithIcon { +.taxAfterList li { display: flex; + align-items: flex-start; + gap: var(--fluss-space-2); + font-size: 0.9375rem; + line-height: 1.55; + color: rgba(255, 255, 255, 0.95); +} + +.taxCheck { + flex-shrink: 0; + display: inline-flex; align-items: center; justify-content: center; + width: 18px; + height: 18px; + margin-top: 2px; + border-radius: 50%; + background: rgba(163, 230, 53, 0.2); + color: var(--fluss-lime); + font-size: 0.75rem; + font-weight: 700; +} + +.taxFootnote { + margin: var(--fluss-space-2) 0 0; + font-family: var(--fluss-font-mono); + font-size: 0.75rem; + letter-spacing: 0.04em; + /* Brighter near-white so the footnote is clearly readable on the + deep-blue tax-section background. Was rgba(219,234,254,0.65), + which read as a faint, low-contrast lavender. */ + color: rgba(255, 255, 255, 0.92); + text-align: center; +} + +/* ========================================================================= + Architecture section (full-width diagram below the hero) + ========================================================================= */ +.archSection { + /* Sits directly below the dark hero, so it inherits a dark canvas. The + gradient fades the cyan/blue accents toward the bottom so the next + (light) section transitions cleanly. */ + padding: var(--fluss-space-20) 0 var(--fluss-space-24); + background: + radial-gradient(1100px 480px at 50% 0%, rgba(38, 109, 149, 0.14), transparent 60%), + radial-gradient(900px 460px at 0% 100%, rgba(38, 109, 149, 0.18), transparent 60%), + linear-gradient(180deg, var(--fluss-blue-950) 0%, var(--fluss-blue-900) 100%); + border-top: 1px solid rgba(255, 255, 255, 0.04); } -.buttonIcon { - margin-right: 8px; - width: 20px; - height: 20px; +/* Mirrors the hero H1's `.accent` four-stop gradient (cyan → blue → violet → + * white) so the architecture title reads as a deliberate continuation of the + * hero, not just a slightly tinted heading. + * + * Selector is scoped under `.archSection` (specificity 0,2,0) deliberately: + * `.sectionDark .sectionTitle { color: #FFFFFF }` would otherwise beat the + * bare `.archTitle` (0,1,0) `color: transparent` and force solid-white text, + * masking the gradient. Matching specificity + later source order wins. */ +.archSection .archTitle { + background: linear-gradient( + 120deg, + var(--fluss-cyan) 0%, + var(--fluss-blue-300) 35%, + #C4B5FD 65%, + #FFFFFF 100% + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +/* Architecture section has no lead paragraph between the H2 and the diagram, + so we collapse the header's bottom spacing and the title's own bottom + margin to put the SVG directly under "Unlocking the Streamhouse Architecture". */ +.archSection .sectionHeader, +.archSection .archTitle { + margin-bottom: 0; +} + +/* Canvas for the SVG diagram. Aspect-ratio matches the SVG viewBox so the + * box reserves the correct height before paint. The diagram is a + * four-column architectural map (sources / hot tier / read patterns / + * engines), so we use a 1200×640 (15:8) viewBox to give each column + * breathing room without crowding labels. + * + * Previously the diagram broke out of its parent .container to reclaim + * page width up to 1480px. On ≥ 1440px displays that gave the architecture + * section visibly narrower side margins than the other homepage sections, + * which all respect the 1240px container (Michael feedback, PR #3226). + * Diagram now stays within the standard container for visual rhythm. */ +.archDiagram { + position: relative; + width: 100%; + aspect-ratio: 1200 / 640; } +.archDiagram svg { + width: 100%; + height: 100%; + overflow: visible; + filter: drop-shadow(0 24px 60px rgba(38, 109, 149, 0.25)); +} diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 0e648d04f6..0d65ca0297 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -17,145 +17,909 @@ import clsx from 'clsx'; import Link from '@docusaurus/Link'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; -import HomepageIntroduce from '@site/src/components/HomepageIntroduce'; -import Heading from '@theme/Heading'; -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; +import {Highlight} from 'prism-react-renderer'; +import flussPrismDark from '@site/src/utils/prismDark'; import styles from './index.module.css'; -function HomepageHeader() { - const {siteConfig} = useDocusaurusContext(); +/** + * Canonical Fluss + Flink SQL snippet, sourced from + * docs/engine-flink/getting-started.md. + */ +const HERO_FLINK_SQL = `-- Register Apache Fluss as a Flink catalog +CREATE CATALOG fluss_catalog WITH ( + 'type' = 'fluss', + 'bootstrap.servers' = 'coordinator-server:9123' +); +USE CATALOG fluss_catalog; + +-- Create a primary-key table +CREATE TABLE pk_table ( + shop_id BIGINT, + user_id BIGINT, + num_orders INT, + PRIMARY KEY (shop_id, user_id) NOT ENFORCED +) WITH ('bucket.num' = '4'); + +INSERT INTO pk_table VALUES (1234, 1234, 1); +SELECT * FROM pk_table WHERE shop_id = 1234; +`; + +/** + * Canonical Fluss + Spark SQL snippet, sourced from + * docs/engine-spark/getting-started.md. Demonstrates registering Fluss + * as a Spark catalog and creating the equivalent primary-key table. + */ +const HERO_SPARK_SQL = `-- Register Apache Fluss as a Spark catalog (via spark-sql --conf): +-- spark.sql.catalog.fluss_catalog = org.apache.fluss.spark.SparkCatalog +-- spark.sql.catalog.fluss_catalog.bootstrap.servers = localhost:9123 +USE fluss_catalog; + +-- Create the same primary-key table +CREATE TABLE pk_table ( + shop_id BIGINT, + user_id BIGINT, + num_orders INT +) TBLPROPERTIES ( + 'primary.key' = 'shop_id,user_id', + 'bucket.num' = '4' +); + +INSERT INTO pk_table VALUES (1234, 1234, 1); +SELECT * FROM pk_table ORDER BY shop_id; +`; + +/** + * Toggle a body-level class while the hero is in view, so we can drive the + * navbar's transparent → solid transition entirely from CSS, without timing + * tricks or pixel-based scroll thresholds. Works on any viewport size. + */ +function useHeroVisibilityClass(ref: React.RefObject) { + useEffect(() => { + const el = ref.current; + if (!el || typeof window === 'undefined') return; + // Mark as on-hero immediately on mount so the navbar starts transparent. + document.body.classList.add('fluss-on-hero'); + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + document.body.classList.add('fluss-on-hero'); + } else { + document.body.classList.remove('fluss-on-hero'); + } + }, + { + // Trigger when the hero has scrolled out far enough that + // ~64px (one navbar height) of it remains under the navbar. + rootMargin: '-64px 0px 0px 0px', + threshold: 0, + }, + ); + observer.observe(el); + return () => { + observer.disconnect(); + document.body.classList.remove('fluss-on-hero'); + }; + }, [ref]); +} + +const SLACK_INVITE = + 'https://join.slack.com/t/apache-fluss/shared_invite/zt-33wlna581-QAooAiCmnYboJS8D_JUcYw'; + +function HeroDiagram() { + // Inline SVG: a four-column architectural map of the Fluss data plane. + // + // 01 · SOURCES (left) — databases, CDC, event logs, IoT + // 02 · FLUSS HOT TIER (centre) — Coordinator + Tablet Servers + // 03 · READ PATTERNS (right) — streaming, batch, lookup, union + // 04 · QUERY ENGINES (bottom) — Flink, Spark, Trino, StarRocks, DuckDB, Ray + // + // The hot tier tiers down to a Lakehouse cold tier (Paimon · Iceberg · + // Lance) via a Tiering Service. ViewBox is 1200 × 640 (15:8) to give the + // four columns enough breathing room without crowding labels. return ( -
-
- - {siteConfig.title} - -

{siteConfig.tagline}

-
- - Quick Start - + + Apache Fluss architecture + + Sources on the left (databases, CDC streams, event logs, + IoT/clickstreams) feed the Fluss hot tier in the centre, + which is composed of a Coordinator Server and a row of + Tablet Servers. Data continuously tiers down to a Lakehouse + cold tier (Apache Paimon, Apache Iceberg, Lance) via a + Tiering Service. Read patterns on the right include + streaming reads, batch reads, lookup joins, and a union + read that merges hot and cold. Query engines along the + bottom include Apache Flink, Apache Spark, Trino, + StarRocks, DuckDB, and Ray. + - - GitHub - GitHub - + + + + + + + +