From 6eb73733e06e3db488d1908a165de8b3be9492c8 Mon Sep 17 00:00:00 2001 From: Andres Contreras Date: Thu, 25 Jun 2026 14:31:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(site):=20GitHub=20Pages=20=E2=80=94=20cosm?= =?UTF-8?q?ic-green=20landing=20+=20MkDocs=20Material=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a public GitHub Pages site with two parts, assembled into _site/ by a single build script used both locally and in CI: - web/ — hand-crafted, brand-matched landing page (firefly hero, stats band, tabbed pre-highlighted code, philosophy, architecture/patterns/ecosystem brand diagrams, 39-module layered grid, quickstart, book, footer). Self-contained, relative URLs, prefers-reduced-motion aware. Install uses the GitHub raw install.sh URL. - mkdocs.yml + overrides/ + docs/stylesheets/pyfly.css + docs/assets/pyfly-mark.svg — MkDocs Material rendering the existing docs/ (layered nav over 43 module + 10 adapter pages), cosmic-green theme, Maven Pro headings, dark mode, search; the docs logo links back to the landing page. - scripts/build_site.py — assembles landing + brand assets + docs build into _site/. - .github/workflows/pages.yml — build + deploy to GitHub Pages. Served at https://fireflyframework.github.io/fireflyframework-pyfly/ (base path aware; easy to repoint at pyfly.io later). Requires a one-time repo setting: Settings -> Pages -> Source: GitHub Actions. --- .github/workflows/pages.yml | 67 +++++ .gitignore | 4 + docs/assets/pyfly-mark.svg | 16 ++ docs/stylesheets/pyfly.css | 94 +++++++ mkdocs.yml | 196 +++++++++++++++ overrides/main.html | 9 + requirements-docs.txt | 4 + scripts/build_site.py | 103 ++++++++ web/app.js | 148 +++++++++++ web/index.html | 489 ++++++++++++++++++++++++++++++++++++ web/styles.css | 404 +++++++++++++++++++++++++++++ 11 files changed, 1534 insertions(+) create mode 100644 .github/workflows/pages.yml create mode 100644 docs/assets/pyfly-mark.svg create mode 100644 docs/stylesheets/pyfly.css create mode 100644 mkdocs.yml create mode 100644 overrides/main.html create mode 100644 requirements-docs.txt create mode 100644 scripts/build_site.py create mode 100644 web/app.js create mode 100644 web/index.html create mode 100644 web/styles.css diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..2fad0b0 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,67 @@ +name: Deploy GitHub Pages + +# Builds the landing page (web/) + docs (MkDocs Material) into _site/ and +# deploys to GitHub Pages. Requires repo Settings → Pages → Source: GitHub Actions. + +on: + push: + branches: [main] + paths: + - "web/**" + - "docs/**" + - "assets/**" + - "overrides/**" + - "mkdocs.yml" + - "scripts/build_site.py" + - "requirements-docs.txt" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress production deploy. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: requirements-docs.txt + + - name: Install docs toolchain + run: pip install -r requirements-docs.txt + + - name: Build site (landing + docs) + run: python scripts/build_site.py + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index a183ad8..8138c9c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ docs/plans/ # Superpowers brainstorming visual companion scratch (local only) .superpowers/ +# GitHub Pages site build output (assembled by scripts/build_site.py) +/_site/ +/site/ + # The book's build sources are real source, not the Python build/ artifact dir !book/build/ !book/build/** diff --git a/docs/assets/pyfly-mark.svg b/docs/assets/pyfly-mark.svg new file mode 100644 index 0000000..ab50438 --- /dev/null +++ b/docs/assets/pyfly-mark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/stylesheets/pyfly.css b/docs/stylesheets/pyfly.css new file mode 100644 index 0000000..cb2d939 --- /dev/null +++ b/docs/stylesheets/pyfly.css @@ -0,0 +1,94 @@ +/* ============================================================ + PyFly docs theme — MkDocs Material, cosmic-green identity. + Mirrors the brand palette in assets/ (banner.svg, diagrams). + ============================================================ */ + +/* ---- custom primary (deep green header in both schemes) ---- */ +[data-md-color-primary="custom"] { + --md-primary-fg-color: #1f5e16; + --md-primary-fg-color--light: #2c8a1c; + --md-primary-fg-color--dark: #143f0f; + --md-primary-bg-color: #f4f9ee; + --md-primary-bg-color--light: #f4f9eeb3; +} + +[data-md-color-accent="custom"] { + --md-accent-fg-color: #4cbb2f; + --md-accent-fg-color--transparent: #4cbb2f1f; + --md-accent-bg-color: #ffffff; + --md-accent-bg-color--light: #ffffffb3; +} + +/* ---- light scheme ---- */ +[data-md-color-scheme="default"] { + --md-typeset-a-color: #2c8a1c; + --md-footer-bg-color: #0a1410; + --md-footer-bg-color--dark: #07110b; +} + +/* ---- dark scheme: cosmic green-tinted ---- */ +[data-md-color-scheme="slate"] { + --md-hue: 145; + --md-default-bg-color: #0b150e; + --md-default-bg-color--light: #0e1812; + --md-default-fg-color: #e9f2e2; + --md-default-fg-color--light: #bacbb0; + --md-default-fg-color--lighter: #85957e; + --md-default-fg-color--lightest: #3a4a33; + --md-code-bg-color: #07110b; + --md-code-fg-color: #cdddc2; + --md-typeset-a-color: #7ed321; + --md-accent-fg-color: #c2e85f; + --md-footer-bg-color: #07110b; + --md-footer-bg-color--dark: #060d08; +} + +/* ---- Maven Pro for headings & chrome (matches the wordmark) ---- */ +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4, +.md-header__title, +.md-nav__title, +.md-tabs__link, +.md-ellipsis { + font-family: "Maven Pro", system-ui, sans-serif; +} +.md-typeset h1, +.md-typeset h2 { letter-spacing: -0.01em; font-weight: 700; } + +/* green title rule under H2 — a brand motif from the README/book */ +.md-typeset h2 { + border-bottom: 1px solid var(--md-default-fg-color--lightest); + padding-bottom: 0.35em; +} +.md-typeset h2::before { + content: ""; + display: inline-block; + width: 0.7em; + height: 0.7em; + margin-right: 0.45em; + vertical-align: -1px; + border-radius: 50%; + background: radial-gradient(circle, #dff58a 0%, #7ed321 45%, rgba(76, 187, 47, 0) 72%); +} + +/* logo: give the firefly mark a soft glow on the header */ +.md-header__button.md-logo img { + filter: drop-shadow(0 0 6px rgba(194, 232, 95, 0.5)); +} + +/* hero/landing back-link affordance in the header title */ +.md-header__title { font-weight: 700; } + +/* code copy + admonition accents stay green via accent var; nudge inline code */ +[data-md-color-scheme="slate"] .md-typeset code { + background-color: rgba(126, 211, 33, 0.1); +} + +/* search highlight + active nav in brand green */ +.md-nav__link--active, +.md-nav__item .md-nav__link--active { color: var(--md-typeset-a-color); } + +/* tables: subtle brand border */ +.md-typeset table:not([class]) th { background: rgba(126, 211, 33, 0.08); } diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..0b609a3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,196 @@ +# MkDocs Material configuration for the PyFly documentation site. +# Renders the existing docs/*.md into https://fireflyframework.github.io/fireflyframework-pyfly/docs/ +# The landing page (web/) and brand assets are assembled around this build by scripts/build_site.py. + +site_name: PyFly +site_description: >- + The official Python implementation of the Firefly Framework — Spring Boot's + cohesion, native to async Python. Dependency injection, CQRS, sagas, event + sourcing and more, with production-ready defaults from day one. +site_author: Firefly Software Foundation +site_url: https://fireflyframework.github.io/fireflyframework-pyfly/docs/ + +repo_url: https://github.com/fireflyframework/fireflyframework-pyfly +repo_name: fireflyframework-pyfly +edit_uri: edit/main/docs/ + +docs_dir: docs +# Keep internal brainstorming specs and the duplicate root index out of the public docs. +exclude_docs: | + /README.md + superpowers/ + +copyright: >- + Apache License 2.0 · © Firefly Software Foundation — + PyFly home + +theme: + name: material + logo: assets/pyfly-mark.svg + favicon: assets/pyfly-mark.svg + custom_dir: overrides + font: + text: Roboto + code: Roboto Mono + icon: + repo: fontawesome/brands/github + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/weather-night + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/weather-sunny + name: Switch to dark mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - navigation.tracking + - navigation.indexes + - navigation.footer + - toc.follow + - search.suggest + - search.highlight + - search.share + - content.code.copy + - content.code.annotate + - content.tabs.link + +extra_css: + - stylesheets/pyfly.css + +extra: + # Make the header logo/title link back to the landing page instead of the docs index. + homepage: https://fireflyframework.github.io/fireflyframework-pyfly/ + social: + - icon: fontawesome/brands/github + link: https://github.com/fireflyframework/fireflyframework-pyfly + name: PyFly on GitHub + - icon: fontawesome/brands/python + link: https://github.com/fireflyframework + name: The Firefly Framework + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + title: On this page + - pymdownx.betterem + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +plugins: + - search + +nav: + - Home: index.md + - Getting Started: getting-started.md + - Installation: installation.md + - Architecture: architecture.md + - CLI Reference: cli.md + - Spring Comparison: spring-comparison.md + - Versioning: versioning.md + - Modules: + - Overview: modules/README.md + - Foundation: + - Core: modules/core.md + - Dependency Injection: modules/dependency-injection.md + - Configuration: modules/configuration.md + - Logging: modules/logging.md + - Error Handling: modules/error-handling.md + - Web & API: + - Web Layer: modules/web.md + - Web Filters: modules/web-filters.md + - Server Layer: modules/server.md + - Validation: modules/validation.md + - Sessions: modules/session.md + - WebSockets: modules/websocket.md + - Internationalization: modules/i18n.md + - Data: + - Data Commons: modules/data.md + - Data Relational (SQL): modules/data-relational.md + - Data Document (MongoDB): modules/data-document.md + - CQRS & Domain: + - CQRS: modules/cqrs.md + - Domain (DDD): modules/domain.md + - Messaging & Eventing: + - Messaging: modules/messaging.md + - Events (EDA): modules/events.md + - Event Sourcing: modules/eventsourcing.md + - Distributed Transactions: + - Transactional Engine: modules/transactional.md + - Business Logic: + - Rule Engine: modules/rule-engine.md + - Plugins: modules/plugins.md + - Security & Identity: + - Security: modules/security.md + - Identity Provider (IDP): modules/idp.md + - Integrations: + - Content Management (ECM): modules/ecm.md + - Notifications: modules/notifications.md + - Callbacks (outbound): modules/callbacks.md + - Webhooks (inbound): modules/webhooks.md + - Resilience & Clients: + - HTTP Client: modules/client.md + - Resilience: modules/resilience.md + - Caching: modules/caching.md + - Scheduling: modules/scheduling.md + - Shell: modules/shell.md + - Config Server: modules/config-server.md + - Observability & Admin: + - Observability: modules/observability.md + - Actuator: modules/actuator.md + - Custom Actuator Endpoints: modules/custom-actuator-endpoints.md + - Admin Dashboard: modules/admin.md + - AOP: modules/aop.md + - Testing: + - Testing: modules/testing.md + - Integration Testing: modules/integration-testing.md + - Starters: modules/starters.md + - Adapters: + - Overview: adapters/README.md + - SQLAlchemy: adapters/sqlalchemy.md + - MongoDB: adapters/mongodb.md + - Starlette: adapters/starlette.md + - FastAPI: adapters/fastapi.md + - Granian: adapters/granian.md + - Kafka: adapters/kafka.md + - RabbitMQ: adapters/rabbitmq.md + - Redis: adapters/redis.md + - HTTPX: adapters/httpx.md + - Click: adapters/click.md diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..45ab235 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + + +{% block extrahead %} + {{ super() }} + + + +{% endblock %} diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..8c5b0c4 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,4 @@ +# Documentation site toolchain (PyFly GitHub Pages). +# Used by scripts/build_site.py and the GitHub Actions Pages workflow. +# pip install -r requirements-docs.txt +mkdocs-material>=9.5,<10 diff --git a/scripts/build_site.py b/scripts/build_site.py new file mode 100644 index 0000000..ad434f6 --- /dev/null +++ b/scripts/build_site.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Assemble the full PyFly GitHub Pages site into ``_site/``. + +Layout produced (served at https://fireflyframework.github.io/fireflyframework-pyfly/): + + _site/ + ├── index.html, styles.css, app.js # the landing page (from web/) + ├── assets/ # brand SVGs + logo (from assets/) + ├── .nojekyll # serve _-prefixed paths verbatim + └── docs/ # MkDocs Material build (from docs/ via mkdocs.yml) + +This is the single source of truth used both locally and by the GitHub Actions +workflow, so what CI deploys is exactly what you can preview on your machine: + + python scripts/build_site.py + python -m http.server -d _site 8000 # then open http://localhost:8000/ +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +WEB = ROOT / "web" +ASSETS = ROOT / "assets" +OUT = ROOT / "_site" + +LANDING_FILES = ("index.html", "styles.css", "app.js") +# Brand assets the landing page references (assets/); copied to _site/assets/. +ASSET_GLOBS = ("*.svg", "pyfly-logo.png") + + +def log(msg: str) -> None: + print(f"[build-site] {msg}") + + +def clean() -> None: + if OUT.exists(): + shutil.rmtree(OUT) + OUT.mkdir(parents=True) + log(f"cleaned {OUT.relative_to(ROOT)}/") + + +def build_docs() -> None: + """Render docs/ into _site/docs via MkDocs Material.""" + target = OUT / "docs" + log("building docs with MkDocs Material …") + try: + subprocess.run( + [sys.executable, "-m", "mkdocs", "build", "--clean", "-d", str(target)], + cwd=ROOT, + check=True, + ) + except FileNotFoundError: + sys.exit("error: mkdocs not found. Install with: pip install -r requirements-docs.txt") + except subprocess.CalledProcessError as exc: + sys.exit(f"error: mkdocs build failed (exit {exc.returncode}).") + log(f"docs → {target.relative_to(ROOT)}/") + + +def copy_landing() -> None: + missing = [f for f in LANDING_FILES if not (WEB / f).exists()] + if missing: + sys.exit(f"error: missing landing files in web/: {', '.join(missing)}") + for name in LANDING_FILES: + shutil.copy2(WEB / name, OUT / name) + log(f"landing → {', '.join(LANDING_FILES)}") + + +def copy_assets() -> None: + dest = OUT / "assets" + dest.mkdir(parents=True, exist_ok=True) + copied = 0 + for pattern in ASSET_GLOBS: + for src in sorted(ASSETS.glob(pattern)): + shutil.copy2(src, dest / src.name) + copied += 1 + if copied == 0: + sys.exit(f"error: no brand assets matched {ASSET_GLOBS} in {ASSETS}") + log(f"assets → {copied} file(s) into assets/") + + +def finalize() -> None: + # .nojekyll keeps GitHub Pages from running Jekyll, which would strip + # any path beginning with an underscore. + (OUT / ".nojekyll").write_text("", encoding="utf-8") + log("wrote .nojekyll") + + +def main() -> None: + clean() + build_docs() + copy_landing() + copy_assets() + finalize() + log(f"done → open {OUT.relative_to(ROOT)}/index.html") + + +if __name__ == "__main__": + main() diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..8e3d332 --- /dev/null +++ b/web/app.js @@ -0,0 +1,148 @@ +/* PyFly landing — interactions: nav, copy, tabs, fireflies, scroll reveal. + Vanilla JS, no dependencies. */ +(function () { + "use strict"; + + /* ---- sticky nav background on scroll ---- */ + var nav = document.getElementById("nav"); + function onScroll() { + if (!nav) return; + nav.classList.toggle("is-scrolled", window.scrollY > 8); + } + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + + /* ---- mobile menu ---- */ + var burger = document.querySelector(".nav__burger"); + if (burger && nav) { + burger.addEventListener("click", function () { + var open = nav.classList.toggle("is-open"); + burger.setAttribute("aria-expanded", String(open)); + }); + nav.querySelectorAll(".nav__links a").forEach(function (a) { + a.addEventListener("click", function () { + nav.classList.remove("is-open"); + burger.setAttribute("aria-expanded", "false"); + }); + }); + } + + /* ---- copy to clipboard ---- */ + function flash(btn, label) { + var original = label ? btn.querySelector(label) : null; + var prev = original ? original.textContent : btn.textContent; + btn.classList.add("is-copied"); + if (original) original.textContent = "Copied!"; + else btn.textContent = "✓"; + setTimeout(function () { + btn.classList.remove("is-copied"); + if (original) original.textContent = prev; + else btn.textContent = prev; + }, 1600); + } + function copyText(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + var ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (e) { /* ignore */ } + document.body.removeChild(ta); + return Promise.resolve(); + } + document.querySelectorAll("[data-copy-text]").forEach(function (btn) { + btn.addEventListener("click", function () { + copyText(btn.getAttribute("data-copy-text")).then(function () { + flash(btn, btn.classList.contains("install__copy") ? ".install__copy-label" : null); + }); + }); + }); + + /* ---- code tabs ---- */ + var tabs = document.querySelectorAll(".code-tab"); + tabs.forEach(function (tab) { + tab.addEventListener("click", function () { + var key = tab.getAttribute("data-tab"); + tabs.forEach(function (t) { + var active = t === tab; + t.classList.toggle("is-active", active); + t.setAttribute("aria-selected", String(active)); + }); + document.querySelectorAll(".code-body").forEach(function (body) { + body.classList.toggle("is-active", body.getAttribute("data-pane") === key); + }); + }); + }); + + /* ---- fireflies ---- */ + var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + function rand(min, max) { return Math.random() * (max - min) + min; } + function spawnFireflies(container, count) { + if (!container) return; + var frag = document.createDocumentFragment(); + for (var i = 0; i < count; i++) { + var f = document.createElement("span"); + f.className = "firefly"; + var size = rand(3, 7); + f.style.width = size + "px"; + f.style.height = size + "px"; + f.style.left = rand(2, 96) + "%"; + f.style.top = rand(6, 92) + "%"; + f.style.setProperty("--d", rand(9, 20).toFixed(1) + "s"); + f.style.setProperty("--delay", "-" + rand(0, 12).toFixed(1) + "s"); + f.style.setProperty("--dx", rand(-40, 40).toFixed(0) + "px"); + f.style.setProperty("--dy", rand(-90, -30).toFixed(0) + "px"); + f.style.setProperty("--peak", rand(0.55, 0.95).toFixed(2)); + frag.appendChild(f); + } + container.appendChild(frag); + } + if (!reduceMotion) { + spawnFireflies(document.getElementById("fireflies"), 26); + spawnFireflies(document.getElementById("fireflies2"), 14); + } else { + spawnFireflies(document.getElementById("fireflies"), 10); + } + + /* ---- scroll reveal ---- */ + var revealTargets = document.querySelectorAll( + ".why-card, .phil-card, .pat-card, .figure, .layer, .step, .code-panel, .book__text, .book__art, .section__title, .lede" + ); + if ("IntersectionObserver" in window && !reduceMotion) { + revealTargets.forEach(function (el) { el.classList.add("reveal"); }); + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add("in"); + io.unobserve(entry.target); + } + }); + }, { threshold: 0.12, rootMargin: "0px 0px -40px 0px" }); + revealTargets.forEach(function (el) { io.observe(el); }); + } + + /* ---- active nav link on scroll ---- */ + var sections = ["why", "code", "architecture", "patterns", "modules"] + .map(function (id) { return document.getElementById(id); }) + .filter(Boolean); + var navLinks = {}; + document.querySelectorAll('.nav__links a[href^="#"]').forEach(function (a) { + navLinks[a.getAttribute("href").slice(1)] = a; + }); + if ("IntersectionObserver" in window && sections.length) { + var spy = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + var link = navLinks[entry.target.id]; + if (link && entry.isIntersecting) { + Object.keys(navLinks).forEach(function (k) { navLinks[k].classList.remove("is-current"); }); + link.classList.add("is-current"); + } + }); + }, { threshold: 0.5 }); + sections.forEach(function (s) { spy.observe(s); }); + } +})(); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..8d58739 --- /dev/null +++ b/web/index.html @@ -0,0 +1,489 @@ + + + + + + PyFly — Spring Boot's cohesion, native to async Python + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + The official Python implementation of the Firefly Framework + +

+ pyFly +

+

Spring Boot's cohesion, native to async Python.

+

+ Build production-grade microservices with the patterns you trust — dependency injection, + CQRS, event-driven architecture, sagas, event sourcing — all integrated, all consistent, + with production-ready defaults from the first commit. +

+ + + +
+ $ curl -fsSL https://raw.githubusercontent.com/fireflyframework/fireflyframework-pyfly/main/install.sh | bash + +
+ +
+ Python 3.12+ + Apache 2.0 + mypy strict + async-first + v26.06.113 +
+
+
+ + +
+
+
39Modules
+
5Layers
+
18Book Chapters
+
Apache 2.0Open Source
+
+
+ + +
+
+

Why PyFly

+

Python gives you infinite choice.
What it doesn't give you is cohesion.

+

+ Every new service starts with two weeks of decisions — web framework, ORM, message broker, + DI wiring, project layout. Six months later a second team makes entirely different choices. + Now you have two codebases with nothing in common. PyFly makes these decisions for you — + one cohesive, full-stack programming model where every module is designed to work together. +

+
+
+
01
+

One cohesive stack

+

DI, HTTP, data, messaging, caching, security, observability — integrated, consistent, and wired automatically. No bespoke glue.

+
+
+
02
+

Not a port — native

+

The same enterprise model Firefly proved on Spring Boot (40+ Java modules), reimagined for async/await, type hints, and Protocols.

+
+
+
03
+

Familiar to Spring devs

+

Stereotypes, auto-configuration, Spring-Data repositories, actuator. A Spring-parity callout at every turn.

+
+
+
+
+ + +
+
+

No boilerplate. No manual wiring.

+

Write the logic. PyFly wires the rest.

+

The DI container resolves dependencies from type hints, validates request bodies, and publishes domain events — all out of the box. Same model, whether you're building a controller, a CQRS bus, or a distributed saga.

+ +
+
+ +
+ + + +
+
+ +
from pyfly.container import rest_controller, service
+from pyfly.web import request_mapping, post_mapping, Body, Valid
+
+@service
+class OrderService:
+    def __init__(self, repo: OrderRepository, events: EventPublisher) -> None:
+        self._repo = repo
+        self._events = events
+
+    async def place_order(self, order: Order) -> Order:
+        saved = await self._repo.save(order)
+        await self._events.publish(OrderPlaced(order_id=saved.id))
+        return saved
+
+@rest_controller
+@request_mapping("/orders")
+class OrderController:
+    def __init__(self, service: OrderService) -> None:
+        self._service = service
+
+    @post_mapping("", status_code=201)
+    async def create(self, order: Valid[Body[Order]]) -> Order:
+        return await self._service.place_order(order)
+ +
from pyfly.cqrs import command, query, CommandHandler, QueryHandler
+
+@command(CreateUser)
+class CreateUserHandler(CommandHandler[CreateUser, int]):
+    def __init__(self, repo: UserRepository) -> None:
+        self._repo = repo
+
+    async def handle(self, cmd: CreateUser) -> int:
+        return (await self._repo.save(User(name=cmd.name, email=cmd.email))).id
+
+@query(FindUserById, cache_ttl=60)  # caching composes declaratively
+class FindUserByIdHandler(QueryHandler[FindUserById, User]):
+    def __init__(self, repo: UserRepository) -> None:
+        self._repo = repo
+
+    async def handle(self, q: FindUserById) -> User:
+        return await self._repo.find_by_id(q.user_id)
+
+# Dispatch — buses are auto-wired by the CqrsAutoConfiguration entry point
+user_id = await commands.dispatch(CreateUser(name="Ada", email="ada@example.com"))
+user = await queries.dispatch(FindUserById(user_id=user_id))
+ +
from pyfly.transactional.saga import saga, saga_step
+from pyfly.transactional.saga.core.context import SagaContext
+
+@saga(name="place-order", timeout_ms=30_000)
+class PlaceOrderSaga:
+    # `compensate=` names a method to invoke if a later step fails.
+    @saga_step(id="reserve-inventory", retry=3, backoff_ms=500, compensate="release_inventory")
+    async def reserve(self, ctx: SagaContext) -> str:
+        return await self._inventory.reserve(ctx.input["order_id"])
+
+    async def release_inventory(self, ctx: SagaContext) -> None:
+        await self._inventory.release(ctx.input["order_id"])
+
+    @saga_step(id="charge-payment", depends_on=["reserve-inventory"], compensate="refund_payment")
+    async def charge(self, ctx: SagaContext) -> str:
+        return await self._payments.charge(ctx.input["order_id"], ctx.input["amount"])
+
+    @saga_step(id="ship-order", depends_on=["charge-payment"])
+    async def ship_order(self, ctx: SagaContext) -> None:
+        await self._ship.dispatch(ctx.input["order_id"])
+
+
+
+ + +
+
+

Philosophy

+

Easy to start. Easy to change. Ready for production.

+
+
+

Convention over Configuration

+

Production-ready defaults for every module. A complete web service is a few lines of YAML — override only what matters.

+
+
+

Your Code, Not Ours

+

Business logic never imports sqlalchemy, redis, or aiokafka. Hexagonal ports & adapters keep infrastructure swappable.

+
+
+

Async-Native, Type-Safe

+

Built for asyncio from the ground up. Every public surface is fully typed and checked by mypy in strict mode.

+
+
+

Production-Ready from Day One

+

Structured logging, correlation IDs, PII redaction, health checks, Prometheus metrics, OWASP headers, graceful shutdown — the baseline.

+
+
+
+
+ + +
+
+

Architecture

+

Cohesive, layered, and hexagonal.

+

Five layers on an async core. Every external system reached through a Protocol port. The right adapters wired automatically at startup — so you can swap PostgreSQL for MongoDB, or Kafka for RabbitMQ, without touching a line of business logic.

+
+ PyFly architecture at a glance: one front door over five layers — Cross-Cutting, Integration, Infrastructure, Application, Foundation — on an async core (asyncio, uvloop, ASGI). +
+
+
+ PyFly hexagonal architecture: an application core depending only on Protocol ports, with swappable adapters implementing each port. +
Ports & adapters — depend on Protocols, swap implementations freely.
+
+
+ PyFly auto-configuration: entry-point discovery, conditional guards, then bind / fall back / skip. +
Auto-configuration — detect installed libraries, bind the right adapters.
+
+
+

Read the architecture guide →

+
+
+ + +
+
+

Featured Patterns

+

More than a web framework.

+

PyFly ships production-grade implementations of the distributed patterns that power real microservices — each a first-class module with port-and-adapter design, CLI scaffolding, metrics, tracing, and persistence.

+
+ PyFly distributed transaction patterns: a saga DAG with reverse-order compensation, alongside Saga, Workflow, and TCC summary cards. +
+
+
+

Saga

+

Distributed transactions with automatic compensation. Parallel DAG execution, per-step retries, idempotency keys, DLQ + recovery.

+ transactional → +
+
+

Workflow

+

Durable, signal-driven orchestration. Wait for signals or timers, spawn child workflows, query while running. Temporal-style, native.

+ transactional → +
+
+

TCC

+

Try / Confirm / Cancel — strong-consistency three-phase transactions for financial workloads where compensation alone isn't enough.

+ transactional → +
+
+

Event Sourcing

+

Append-only event log, aggregates that rebuild from history, snapshots, transactional outbox, projections, upcasting.

+ eventsourcing → +
+
+

CQRS

+

Command/Query buses with validation, authorization, and caching composing declaratively as cross-cutting concerns.

+ cqrs → +
+
+

Rule Engine

+

Externalize business rules in a YAML DSL with AST evaluation, audit trails, batch evaluation, and hot reload.

+ rule-engine → +
+
+
+
+ + +
+
+

Modules

+

39 modules across 5 layers.

+

Everything from HTTP routing and database access to distributed transactions, identity, content management, and DDD building blocks.

+
+
+
Foundation
+
+ corekernelcontainercontextconfiglogging +
+
+
+
Application
+
+ webserverdatadata-relationaldata-documentcqrsvalidation +
+
+
+
Infrastructure
+
+ securitymessagingedacacheclientschedulingresilienceshelltransactionaleventsourcingdomainpluginsrule-engineconfig-server +
+
+
+
Integration
+
+ idpecmnotificationscallbackswebhooksstarters +
+
+
+
Cross-Cutting
+
+ aopobservabilityactuatoradmintestingcli +
+
+
+

Browse all module guides →

+
+
+ + +
+
+

Quickstart

+

Zero to a running service in under a minute.

+
    +
  1. +
    1
    +
    +

    Install the CLI + framework

    +
    curl -fsSL https://raw.githubusercontent.com/fireflyframework/fireflyframework-pyfly/main/install.sh | bash
    +
    +
  2. +
  3. +
    2
    +
    +

    Scaffold a REST API with batteries included

    +
    pyfly new my-service --archetype web-api && cd my-service
    +
    +
  4. +
  5. +
    3
    +
    +

    Run it — logging, health, metrics & OpenAPI all on by default

    +
    pyfly run --reload
    +
    +
  6. +
  7. +
    4
    +
    +

    It's live

    +
    curl http://localhost:8080/health # {"status":"UP"}
    +

    OpenAPI / Swagger UI at http://localhost:8080/docs

    +
    +
  8. +
+ +
+
+ + +
+
+
+

The Book

+

PyFly by Example

+

The official, project-driven book. Across 18 chapters in five parts it builds Lumen — a real wallet & ledger microservice — from an empty folder into a secured, observable, event-driven service. Every listing is drawn from a running project. Spring developers get a parity callout at every turn.

+ +
+ +
+
+ + +
+
+

The Firefly Ecosystem

+

One programming model across every runtime.

+

PyFly is the green firefly of the Firefly Framework family — the same cohesive model on Java/Spring Boot, .NET, Rust, Go, and Angular.

+
+ The Firefly Framework family: Java/Spring Boot, .NET, PyFly (Python, highlighted), Rust, Go CLI, Angular frontend, and GenAI — all sharing one programming model. +
+
+
+ + +
+ +
+

Build production-grade Python apps — from the first commit.

+ +
+
+
+ + + + + + + diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..cf13df4 --- /dev/null +++ b/web/styles.css @@ -0,0 +1,404 @@ +/* ============================================================ + PyFly — landing page styles + Cosmic-green identity: dark sky, green fireflies, Maven Pro wordmark. + Palette + motifs mirror assets/ (banner.svg, brand diagrams). + ============================================================ */ + +:root { + /* sky */ + --sky-0: #07110b; + --sky-1: #0a1410; + --sky-2: #0c1a0f; + /* surfaces & lines */ + --surface: #0e1812; + --surface-2: #102018; + --line: rgba(126, 211, 33, 0.16); + --line-soft: rgba(233, 242, 226, 0.09); + /* greens */ + --lime: #7ed321; + --green: #4cbb2f; + --green-2: #43b02a; + --green-deep: #1f5e16; + /* firefly glow */ + --glow-a: #9bd24a; + --glow-b: #c2e85f; + --glow-c: #dff58a; + /* warm accent (from diagram palette) */ + --amber: #e0a45c; + /* ink */ + --ink: #e9f2e2; + --ink-2: #bacbb0; + --ink-3: #85957e; + /* light panel (for embedded diagrams) */ + --panel: #f4f9ee; + --panel-ink: #33402e; + --panel-line: #dfe7d4; + + --font-display: "Maven Pro", system-ui, sans-serif; + --font-body: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + + --maxw: 1120px; + --radius: 16px; + --radius-sm: 10px; + --shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.8); + --glow-shadow: 0 0 40px -8px rgba(126, 211, 33, 0.35); +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; scroll-padding-top: 84px; } + +body { + margin: 0; + font-family: var(--font-body); + background: var(--sky-0); + color: var(--ink); + line-height: 1.65; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: hidden; +} + +h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; line-height: 1.12; letter-spacing: -0.01em; } + +a { color: var(--lime); text-decoration: none; transition: color 0.15s ease; } +a:hover { color: var(--glow-c); } + +img { max-width: 100%; display: block; } + +code { font-family: var(--font-mono); font-size: 0.92em; } + +.skip-link { + position: absolute; left: -999px; top: 8px; z-index: 200; + background: var(--lime); color: #04220a; padding: 10px 16px; border-radius: 8px; font-weight: 700; +} +.skip-link:focus { left: 12px; } + +:focus-visible { outline: 2px solid var(--lime); outline-offset: 3px; border-radius: 4px; } + +.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; } +.center { text-align: center; } + +/* ============================== NAV ============================== */ +.nav { + position: sticky; top: 0; z-index: 100; + transition: background 0.25s ease, border-color 0.25s ease, backdrop-filter 0.25s ease; + border-bottom: 1px solid transparent; +} +.nav.is-scrolled { + background: rgba(7, 17, 11, 0.82); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom-color: var(--line); +} +.nav__inner { + max-width: var(--maxw); margin: 0 auto; padding: 14px 24px; + display: flex; align-items: center; gap: 28px; +} +.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); font-family: var(--font-display); font-weight: 800; font-size: 1.32rem; } +.brand:hover { color: var(--ink); } +.brand__spark { + width: 12px; height: 12px; border-radius: 50%; + background: radial-gradient(circle, var(--glow-c) 0%, var(--lime) 45%, rgba(76, 187, 47, 0) 72%); + box-shadow: 0 0 12px 2px rgba(194, 232, 95, 0.7); +} +.brand__word { letter-spacing: -0.02em; } +.brand__word-cap { color: var(--lime); } + +.nav__links { display: flex; gap: 26px; margin-left: 4px; } +.nav__links a { color: var(--ink-2); font-size: 0.95rem; font-weight: 500; } +.nav__links a:hover { color: var(--ink); } +.nav__links a.is-current { color: var(--lime); } +.nav__actions { margin-left: auto; display: flex; align-items: center; gap: 14px; } +.ghlink { color: var(--ink-2); display: inline-flex; } +.ghlink:hover { color: var(--lime); } + +.nav__burger { display: none; background: none; border: 0; cursor: pointer; padding: 8px; flex-direction: column; gap: 5px; } +.nav__burger span { width: 22px; height: 2px; background: var(--ink); border-radius: 2px; transition: 0.2s; } + +/* ============================== BUTTONS ============================== */ +.btn { + display: inline-flex; align-items: center; justify-content: center; + padding: 12px 22px; border-radius: 999px; font-weight: 700; font-size: 0.96rem; + font-family: var(--font-display); cursor: pointer; border: 1px solid transparent; + transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease; + white-space: nowrap; +} +.btn:active { transform: translateY(1px); } +.btn--primary { + background: linear-gradient(135deg, #b6f06a 0%, var(--lime) 45%, var(--green-2) 100%); + color: #06250b; box-shadow: var(--glow-shadow); +} +.btn--primary:hover { color: #06250b; box-shadow: 0 0 52px -6px rgba(126, 211, 33, 0.55); transform: translateY(-1px); } +.btn--ghost { background: rgba(233, 242, 226, 0.04); color: var(--ink); border-color: var(--line); } +.btn--ghost:hover { color: var(--ink); border-color: var(--lime); background: rgba(126, 211, 33, 0.08); } + +/* ============================== HERO ============================== */ +.hero { position: relative; padding: 96px 0 88px; overflow: hidden; } +.hero__sky { position: absolute; inset: 0; z-index: 0; background: linear-gradient(135deg, var(--sky-0) 0%, var(--sky-1) 50%, var(--sky-2) 100%); } +.hero__ambient { + position: absolute; top: -180px; right: -120px; width: 760px; height: 760px; + background: radial-gradient(circle, rgba(76, 187, 47, 0.22) 0%, rgba(126, 211, 33, 0.07) 48%, rgba(126, 211, 33, 0) 72%); + pointer-events: none; +} +.hero::after { /* bottom fade into page */ + content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 160px; z-index: 1; + background: linear-gradient(to bottom, rgba(7, 17, 11, 0), var(--sky-0)); pointer-events: none; +} +.hero__content { position: relative; z-index: 2; max-width: 880px; margin: 0 auto; padding: 0 24px; text-align: center; } + +.pill { + display: inline-flex; align-items: center; gap: 9px; + padding: 7px 16px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; + color: var(--ink-2); background: rgba(126, 211, 33, 0.07); border: 1px solid var(--line); +} +.pill:hover { color: var(--ink); } +.pill__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--glow-b); box-shadow: 0 0 10px 1px var(--glow-b); } + +.hero__title { margin: 26px 0 8px; } +.wordmark { + font-family: var(--font-display); font-weight: 800; font-size: clamp(4rem, 13vw, 8.5rem); + line-height: 0.95; letter-spacing: -0.03em; + background: linear-gradient(180deg, #b6f06a 0%, var(--lime) 50%, var(--green-2) 100%); + -webkit-background-clip: text; background-clip: text; color: transparent; + filter: drop-shadow(0 6px 30px rgba(126, 211, 33, 0.25)); +} +.wordmark__cap { -webkit-text-fill-color: #eaf6d6; color: #eaf6d6; } + +.hero__tagline { font-family: var(--font-display); font-weight: 600; font-size: clamp(1.5rem, 3.4vw, 2.1rem); margin: 6px 0 18px; color: var(--ink); } +.accent { color: var(--lime); } +.hero__sub { font-size: clamp(1.02rem, 1.8vw, 1.18rem); color: var(--ink-2); max-width: 660px; margin: 0 auto 32px; } + +.hero__cta { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; } + +.install { + margin: 34px auto 0; max-width: 660px; + display: flex; align-items: flex-start; gap: 12px; + background: rgba(4, 10, 6, 0.6); border: 1px solid var(--line); border-radius: var(--radius-sm); + padding: 12px 12px 12px 16px; +} +.install__code { color: var(--ink); font-size: 0.85rem; flex: 1; text-align: left; white-space: normal; overflow-wrap: anywhere; line-height: 1.65; } +.install__prompt { color: var(--lime); margin-right: 10px; user-select: none; } +.install__copy { + display: inline-flex; align-items: center; gap: 6px; cursor: pointer; flex: none; align-self: flex-start; + background: rgba(126, 211, 33, 0.1); color: var(--ink); border: 1px solid var(--line); + border-radius: 7px; padding: 8px 12px; font-size: 0.82rem; font-weight: 600; font-family: var(--font-display); + transition: 0.15s; +} +.install__copy:hover { background: rgba(126, 211, 33, 0.2); border-color: var(--lime); } +.install__copy.is-copied { color: var(--lime); } + +.hero__badges { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 28px; } +.badge { + font-family: var(--font-mono); font-size: 0.76rem; color: var(--ink-2); + padding: 5px 12px; border-radius: 999px; border: 1px solid var(--line-soft); background: rgba(233, 242, 226, 0.03); +} +.badge--ver { color: var(--lime); border-color: var(--line); } + +/* ============================== FIREFLIES ============================== */ +.fireflies { position: absolute; inset: 0; pointer-events: none; } +.firefly { + position: absolute; border-radius: 50%; + background: radial-gradient(circle, var(--glow-c) 0%, var(--glow-a) 45%, rgba(155, 210, 74, 0) 70%); + opacity: 0; will-change: transform, opacity; + animation: ff-drift var(--d, 14s) ease-in-out infinite, ff-flicker calc(var(--d, 14s) / 3.5) ease-in-out infinite; + animation-delay: var(--delay, 0s); +} +@keyframes ff-drift { + 0% { transform: translate(0, 0); } + 50% { transform: translate(var(--dx, 20px), calc(var(--dy, -50px))); } + 100% { transform: translate(0, 0); } +} +@keyframes ff-flicker { + 0%, 100% { opacity: 0.15; } + 50% { opacity: var(--peak, 0.85); } +} + +/* ============================== SECTIONS ============================== */ +.section { padding: 84px 0; position: relative; } +.section--tint { background: linear-gradient(180deg, var(--sky-0), var(--surface) 50%, var(--sky-0)); } +.eyebrow { + text-transform: uppercase; letter-spacing: 0.16em; font-size: 0.78rem; font-weight: 700; + color: var(--lime); margin: 0 0 14px; +} +.eyebrow::before { content: "◆ "; color: var(--green-2); } +.section__title { font-size: clamp(1.8rem, 4vw, 2.7rem); margin: 0 0 20px; max-width: 18ch; } +#why .section__title, #code .section__title { max-width: 22ch; } +.lede { font-size: clamp(1.04rem, 1.6vw, 1.18rem); color: var(--ink-2); max-width: 70ch; margin: 0 0 38px; } +.lede strong { color: var(--ink); } + +/* ============================== WHY ============================== */ +.why-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; } +.why-card { + background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); + padding: 28px 26px; position: relative; overflow: hidden; +} +.why-card::before { + content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; + background: linear-gradient(90deg, var(--lime), rgba(126, 211, 33, 0)); +} +.why-card__num { font-family: var(--font-mono); font-size: 0.9rem; color: var(--green-deep); font-weight: 700; + display: inline-grid; place-items: center; width: 38px; height: 38px; border-radius: 10px; + background: rgba(126, 211, 33, 0.12); color: var(--glow-c); margin-bottom: 16px; } +.why-card h3 { font-size: 1.2rem; margin: 0 0 8px; } +.why-card p { color: var(--ink-2); margin: 0; font-size: 0.98rem; } +.why-card code, .phil-card code, .step__note code { color: var(--glow-c); background: rgba(126, 211, 33, 0.1); padding: 1px 6px; border-radius: 5px; } + +/* ============================== CODE PANEL ============================== */ +.code-panel { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: #060d08; box-shadow: var(--shadow); } +.code-top { display: flex; align-items: stretch; gap: 14px; background: rgba(0, 0, 0, 0.35); padding-left: 18px; border-bottom: 1px solid var(--line-soft); } +.code-dots { display: inline-flex; align-items: center; gap: 7px; } +.code-dots i { width: 11px; height: 11px; border-radius: 50%; display: block; } +.code-dots i:nth-child(1) { background: #5a4326; } +.code-dots i:nth-child(2) { background: #4f4a22; } +.code-dots i:nth-child(3) { background: #275024; } +.code-tabs { display: flex; gap: 2px; padding: 8px 8px 0; overflow-x: auto; flex: 1; } +.code-tab { + background: none; border: 0; cursor: pointer; color: var(--ink-3); font-family: var(--font-display); font-weight: 600; + font-size: 0.92rem; padding: 11px 18px; border-radius: 9px 9px 0 0; white-space: nowrap; transition: 0.15s; +} +.code-tab:hover { color: var(--ink-2); } +.code-tab.is-active { color: var(--ink); background: #060d08; box-shadow: inset 0 2px 0 var(--lime); } +.code-body { display: none; } +.code-body.is-active { display: block; } +.code-body pre { margin: 0; padding: 24px 26px; overflow-x: auto; font-size: 0.86rem; line-height: 1.7; } +.code-body code { font-family: var(--font-mono); color: #cdddc2; } + +/* syntax tokens (green-forward, amber strings) */ +.c-kw { color: #8fe34a; } +.c-dec { color: var(--glow-c); font-weight: 600; } +.c-str { color: var(--amber); } +.c-com { color: #6b7d62; font-style: italic; } +.c-fn { color: #b6f06a; } +.c-cls { color: #eaf6d6; } +.c-num { color: #d6e89a; } +.c-self { color: var(--ink-3); font-style: italic; } + +/* ============================== PHILOSOPHY ============================== */ +.phil-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; } +.phil-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 28px; transition: border-color 0.2s ease, transform 0.2s ease; } +.phil-card:hover { border-color: var(--lime); transform: translateY(-2px); } +.phil-card h3 { font-size: 1.18rem; margin: 0 0 10px; color: var(--lime); } +.phil-card p { color: var(--ink-2); margin: 0; } + +/* ============================== FIGURES ============================== */ +.figure { margin: 0 0 18px; background: var(--panel); border: 1px solid var(--panel-line); border-radius: var(--radius); padding: 22px; box-shadow: var(--shadow); } +.figure img { margin: 0 auto; } +.figure figcaption { margin-top: 14px; text-align: center; color: var(--panel-ink); font-size: 0.9rem; font-weight: 600; } +.figure-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; } +.figure-note { text-align: center; margin: 8px 0 0; } +.figure-note a { font-weight: 700; font-family: var(--font-display); } + +/* ============================== PATTERNS ============================== */ +.pat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-top: 8px; } +.pat-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px; display: flex; flex-direction: column; transition: border-color 0.2s, transform 0.2s; } +.pat-card:hover { border-color: var(--lime); transform: translateY(-2px); } +.pat-card h3 { font-size: 1.16rem; margin: 0 0 10px; } +.pat-card p { color: var(--ink-2); margin: 0 0 16px; font-size: 0.96rem; flex: 1; } +.pat-card a { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 600; } + +/* ============================== MODULES / LAYERS ============================== */ +.layers { display: flex; flex-direction: column; gap: 14px; } +.layer { display: grid; grid-template-columns: 160px 1fr; gap: 18px; align-items: center; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px 22px; } +.layer__label { font-family: var(--font-display); font-weight: 700; color: var(--lime); font-size: 1.02rem; } +.chips { display: flex; flex-wrap: wrap; gap: 8px; } +.chip { font-family: var(--font-mono); font-size: 0.8rem; color: var(--ink-2); background: rgba(233, 242, 226, 0.04); border: 1px solid var(--line-soft); border-radius: 7px; padding: 4px 10px; transition: 0.15s; } +.chip:hover { color: var(--glow-c); border-color: var(--line); background: rgba(126, 211, 33, 0.08); } + +/* ============================== QUICKSTART ============================== */ +.steps { list-style: none; margin: 0 0 36px; padding: 0; display: flex; flex-direction: column; gap: 18px; max-width: 820px; } +.step { display: flex; gap: 18px; align-items: flex-start; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 22px 24px; } +.step__num { flex: none; width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; font-family: var(--font-display); font-weight: 800; color: #06250b; background: linear-gradient(135deg, var(--glow-c), var(--lime)); } +.step__body { flex: 1; min-width: 0; } +.step__body h3 { font-size: 1.08rem; margin: 4px 0 12px; } +.step__note { color: var(--ink-3); font-size: 0.9rem; margin: 10px 0 0; } +.mini-code { display: flex; align-items: center; gap: 10px; background: #060d08; border: 1px solid var(--line-soft); border-radius: 9px; padding: 10px 12px; overflow-x: auto; } +.mini-code code { color: var(--ink); white-space: nowrap; flex: 1; } +.mini-copy { flex: none; cursor: pointer; background: none; border: 0; color: var(--ink-3); font-size: 1rem; padding: 2px 6px; border-radius: 6px; transition: 0.15s; } +.mini-copy:hover { color: var(--lime); background: rgba(126, 211, 33, 0.1); } +.mini-copy.is-copied { color: var(--lime); } + +/* ============================== BOOK ============================== */ +.section--book { background: linear-gradient(180deg, var(--sky-0), #0a160d 50%, var(--sky-0)); } +.book { display: grid; grid-template-columns: 1.3fr 0.7fr; gap: 48px; align-items: center; } +.book__text .section__title { max-width: none; } +.book__art { display: grid; place-items: center; } +.book__cover { + width: 230px; height: 320px; border-radius: 12px; position: relative; overflow: hidden; + background: linear-gradient(155deg, var(--sky-1) 0%, #0c2012 60%, #0a3015 100%); + border: 1px solid var(--line); box-shadow: var(--shadow), inset 0 0 60px rgba(76, 187, 47, 0.12); + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; + transform: rotate(-4deg); transition: transform 0.3s ease; +} +.book__cover:hover { transform: rotate(0deg) scale(1.02); } +.book__cover-spark { width: 16px; height: 16px; border-radius: 50%; margin-bottom: 14px; + background: radial-gradient(circle, var(--glow-c), var(--lime) 50%, rgba(76, 187, 47, 0) 72%); + box-shadow: 0 0 22px 3px rgba(194, 232, 95, 0.7); } +.book__cover-title { font-family: var(--font-display); font-weight: 800; font-size: 2.6rem; letter-spacing: -0.03em; + background: linear-gradient(180deg, #b6f06a, var(--lime)); -webkit-background-clip: text; background-clip: text; color: transparent; } +.book__cover-title .wordmark__cap { -webkit-text-fill-color: #eaf6d6; color: #eaf6d6; } +.book__cover-sub { font-family: var(--font-display); color: var(--ink-2); font-size: 1rem; letter-spacing: 0.04em; } + +/* ============================== CTA BAND ============================== */ +.cta-band { position: relative; padding: 92px 0; overflow: hidden; text-align: center; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.cta-band__sky { position: absolute; inset: 0; background: radial-gradient(ellipse at center, #0d1d12 0%, var(--sky-0) 75%); } +.cta-band__inner { position: relative; z-index: 2; } +.cta-band h2 { font-size: clamp(1.7rem, 3.6vw, 2.5rem); margin: 0 0 28px; max-width: 20ch; margin-inline: auto; } + +/* ============================== FOOTER ============================== */ +.footer { background: var(--sky-0); padding: 64px 0 48px; border-top: 1px solid var(--line-soft); } +.footer__inner { display: grid; grid-template-columns: 1.4fr 2fr; gap: 48px; } +.footer__brand .brand__word { font-family: var(--font-display); font-weight: 800; font-size: 1.5rem; color: var(--ink); } +.footer__brand .brand__word-cap { color: var(--lime); } +.footer__brand p { color: var(--ink-3); font-size: 0.92rem; max-width: 36ch; margin: 12px 0 0; } +.footer__copy { font-size: 0.82rem !important; } +.footer__cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; } +.footer__col h4 { font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-3); margin: 0 0 14px; } +.footer__col a { display: block; color: var(--ink-2); font-size: 0.92rem; padding: 5px 0; } +.footer__col a:hover { color: var(--lime); } + +/* ============================== REVEAL ============================== */ +.reveal { opacity: 0; transform: translateY(18px); transition: opacity 0.6s ease, transform 0.6s ease; } +.reveal.in { opacity: 1; transform: none; } + +/* ============================== STATS BAND ============================== */ +.stats { + border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); + background: linear-gradient(180deg, var(--sky-0), var(--surface) 50%, var(--sky-0)); +} +.stats__inner { display: flex; flex-wrap: wrap; justify-content: space-around; gap: 24px; padding: 32px 24px; } +.stat { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 120px; } +.stat__num { + font-family: var(--font-display); font-weight: 800; font-size: clamp(1.9rem, 4vw, 2.7rem); line-height: 1; + background: linear-gradient(180deg, #b6f06a, var(--lime)); -webkit-background-clip: text; background-clip: text; color: transparent; +} +.stat__label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.13em; color: var(--ink-3); } + +/* ============================== RESPONSIVE ============================== */ +@media (max-width: 900px) { + .why-grid, .pat-grid { grid-template-columns: 1fr 1fr; } + .footer__inner { grid-template-columns: 1fr; gap: 32px; } +} +@media (max-width: 760px) { + .nav__links { display: none; position: absolute; top: 100%; left: 0; right: 0; flex-direction: column; gap: 0; + background: rgba(7, 17, 11, 0.97); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); padding: 8px 0; } + .nav.is-open .nav__links { display: flex; } + .nav__links a { padding: 14px 24px; } + .nav__burger { display: flex; } + .why-grid, .phil-grid, .pat-grid, .figure-pair { grid-template-columns: 1fr; } + .stat { min-width: 40%; } + .layer { grid-template-columns: 1fr; gap: 12px; } + .book { grid-template-columns: 1fr; gap: 32px; } + .book__art { order: -1; } + .section { padding: 64px 0; } + .hero { padding: 64px 0 56px; } +} + +/* ============================== MOTION PREFERENCES ============================== */ +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .firefly { animation: none; opacity: 0.55; } + .reveal { opacity: 1; transform: none; transition: none; } + .btn, .phil-card, .pat-card, .book__cover { transition: none; } +}