From ee59780c068eeadcd34b4ef125fd3ae23c45093f Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sun, 19 Jul 2026 19:43:52 -0300 Subject: [PATCH 1/4] feat(landing): rebuild home into an ecosystem-integrated experience Turn the single-hero landing page into a full developer/researcher funnel that surfaces the whole RocketPy ecosystem. Sections: sticky nav (text labels + persistent Simulate CTA + live GitHub star badge + mobile menu); reworked hero with `pip install rocketpy`, CTA cluster and micro social-proof; live stats band; four-class quickstart with syntax-highlighted, copyable code and an Open-in-Colab link; output showcase; auto-scrolling features carousel; validation + used-by wall; ecosystem cards (RocketSerializer / Infinity-API / App / Docs); R&D roadmap; community; how -to-cite with copyable BibTeX; final CTA; and an expanded footer with link columns. Live stats (GitHub stars/forks/contributors, recent PyPI downloads) are fetched at build time by scripts/fetch-stats.mjs and injected into dist, with static fallbacks and continue-on-error so a flaky API never breaks a deploy. A monthly cron redeploy keeps the numbers fresh. New css/home.css reuses the about.css component recipes so both pages read as one system; footer columns are additive so the About footer is unchanged. Accessibility: prefers-reduced-motion, skip link, focus rings and a noscript fallback. No new npm dependencies. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/static.yml | 11 + css/footer.css | 139 +++ css/home.css | 1219 +++++++++++++++++++++++++ css/main.css | 77 ++ images/showcase/altitude-speed.svg | 55 ++ images/showcase/flight-3d.svg | 54 ++ images/showcase/stability-margin.svg | 39 + index.html | 1251 ++++++++++++++++++++++---- package.json | 4 +- scripts/fetch-stats.mjs | 126 +++ sitemap.xml | 4 +- 11 files changed, 2807 insertions(+), 172 deletions(-) create mode 100644 css/home.css create mode 100644 images/showcase/altitude-speed.svg create mode 100644 images/showcase/flight-3d.svg create mode 100644 images/showcase/stability-margin.svg create mode 100644 scripts/fetch-stats.mjs diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index c0203fd..84f49e0 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -9,6 +9,12 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: + # Monthly redeploy (1st of each month, 06:00 UTC) so the live stats injected + # at build time (GitHub stars/forks/contributors, PyPI downloads) stay fresh + # without needing a push. Scheduled runs always execute on the default branch. + schedule: + - cron: "0 6 1 * *" + # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read @@ -40,6 +46,11 @@ jobs: run: npm ci - name: Build site run: npm run build + - name: Refresh live stats + run: npm run stats + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true - name: Add .nojekyll run: touch dist/.nojekyll - name: Setup Pages diff --git a/css/footer.css b/css/footer.css index 3d2b43a..d762fad 100644 --- a/css/footer.css +++ b/css/footer.css @@ -191,3 +191,142 @@ margin-left: 0; } } + +/* ========================================================================== + EXPANDED SITE FOOTER (landing page) — additive; the legacy + .footer-left/.footer-center/.footer-right rules above still drive the + About page footer unchanged. + ========================================================================== */ +.site-footer { + padding: clamp(2.5rem, 5vw, 3.5rem) clamp(1.25rem, 4vw, 3rem) 1.8rem; + text-align: left; + font: + normal 16px "Open Sans", + sans-serif; +} + +.footer-top { + display: grid; + grid-template-columns: minmax(220px, 1fr) 2.2fr; + gap: clamp(2rem, 5vw, 4rem); + max-width: 1240px; + margin: 0 auto; +} + +.footer-brand .footer-logo { + margin-bottom: 14px; +} + +.footer-tagline { + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + line-height: 1.6; + color: rgba(255, 255, 255, 0.7); + max-width: 280px; + margin: 0; +} + +.footer-social { + display: flex; + gap: 10px; + margin-top: 18px; +} + +.footer-social a { + width: 38px; + height: 38px; + border-radius: 8px; + background: #33383b; + color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 18px; + text-decoration: none; + transition: + background 0.2s ease, + transform 0.2s ease; +} + +.footer-social a:hover, +.footer-social a:focus-visible { + background: #4a5568; + transform: translateY(-2px); +} + +.footer-links-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; +} + +.footer-col h4 { + font-family: "Rubik", sans-serif; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #ffffff; + margin: 0 0 12px; +} + +.footer-col a { + display: block; + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + font-weight: 400; + color: rgba(255, 255, 255, 0.68); + text-decoration: none; + padding: 6px 0; + transition: color 0.2s ease; +} + +.footer-col a:hover, +.footer-col a:focus-visible { + color: #ffffff; +} + +.footer-bottom { + max-width: 1240px; + margin: clamp(2rem, 4vw, 2.6rem) auto 0; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 10px; +} + +.footer-bottom p { + font-family: "Open Sans", sans-serif; + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.6); + margin: 0; +} + +.footer-bottom a { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.footer-bottom a:hover { + color: #ffffff; +} + +@media (max-width: 880px) { + .footer-top { + grid-template-columns: 1fr; + } + + .footer-bottom { + justify-content: center; + text-align: center; + } +} + +@media (max-width: 620px) { + .footer-links-grid { + grid-template-columns: 1fr 1fr; + } +} diff --git a/css/home.css b/css/home.css new file mode 100644 index 0000000..f5ea16d --- /dev/null +++ b/css/home.css @@ -0,0 +1,1219 @@ +/* ========================================================================== + ROCKETPY LANDING PAGE — HOME SECTIONS (home.css) + Loaded only by index.html, after main.css + footer.css. + Reuses main.css design tokens and ports the on-brand component recipes + first authored in about.css (stat row, featured cards, institution grid, + CTA cluster) so the landing page and the team page read as one system. + ========================================================================== */ + +/* ---- tokens ported from about.css (main.css owns a disjoint set, so these + only ADD; they never override main.css and never leak — home.css is + not loaded on about.html). ------------------------------------------- */ +:root { + --accent: #f0ab50; + --accent-soft: #ffd38f; + --accent-teal: #2dd4bf; + --accent-teal-soft: #99f6e4; + --panel: rgba(33, 35, 50, 0.45); + --panel-strong: rgba(33, 35, 50, 0.65); + --hairline: rgba(255, 255, 255, 0.08); + --hairline-strong: rgba(255, 255, 255, 0.18); + --ink-soft: rgba(255, 255, 255, 0.78); + --ink-faint: rgba(255, 255, 255, 0.6); + --code-bg: #0b1226; +} + +/* Compensate for the sticky header when jumping to an in-page anchor. */ +section[id], +:target { + scroll-margin-top: 92px; +} + +/* small layout helpers (keep markup free of inline styles) */ +.align-left { + text-align: left; +} +.justify-start { + justify-content: flex-start; +} +.mt-content { + margin-top: var(--space-content); +} + +/* Overflow guard: grid/flex children default to min-width:auto, so a wide + child (the code block's long lines) would stretch the page instead of + scrolling. Let them shrink and keep their own overflow-x:auto. */ +.split-layout > * { + min-width: 0; +} +.code-block { + max-width: 100%; +} + +/* ============================ STICKY HEADER ============================== */ +.site-header { + position: sticky; + top: 8px; + z-index: 50; + display: flex; + align-items: center; + gap: 16px; + padding: 10px 12px 10px 18px; + margin-bottom: var(--space-section); + border-radius: 14px; + background: rgba(6, 13, 38, 0.72); + border: 1px solid var(--surface-border); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.32); + -webkit-backdrop-filter: blur(14px); + backdrop-filter: blur(14px); +} + +.site-header .brand-link { + display: inline-flex; + flex-shrink: 0; +} + +.site-header .brand-logo { + height: 40px; + width: 184px; +} + +.nav-links { + display: flex; + align-items: center; + gap: 2px; + margin: 0 auto 0 8px; +} + +.nav-link { + font-family: "Ruda", sans-serif; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.02em; + color: rgba(255, 255, 255, 0.82); + text-decoration: none; + padding: 8px 13px; + border-radius: 8px; + transition: + color 0.2s ease, + background 0.2s ease; +} + +.nav-link:hover, +.nav-link:focus-visible { + color: #ffffff; + background: rgba(255, 255, 255, 0.07); +} + +.nav-actions { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} + +.star-badge { + display: inline-flex; + align-items: center; + gap: 7px; + font-family: "Ruda", sans-serif; + font-size: 13px; + font-weight: 700; + color: #ffffff; + background: var(--icon-bg); + border: 1px solid var(--surface-border); + border-radius: 999px; + padding: 7px 13px; + text-decoration: none; + white-space: nowrap; + transition: + border-color 0.2s ease, + transform 0.2s ease; +} + +.star-badge:hover, +.star-badge:focus-visible { + border-color: rgba(255, 255, 255, 0.34); + transform: translateY(-1px); +} + +.star-badge .fa-star { + color: var(--accent-strong); + font-size: 12px; +} + +.star-badge .fa-github { + font-size: 15px; +} + +.nav-cta { + min-height: 40px; + padding: 8px 18px; + font-size: 13.5px; +} + +.nav-toggle { + display: none; + align-items: center; + justify-content: center; + width: 42px; + height: 40px; + border-radius: 10px; + border: 1px solid var(--surface-border); + background: var(--icon-bg); + color: #ffffff; + font-size: 18px; + cursor: pointer; +} + +/* =============================== HERO ==================================== */ +.hero-sub { + margin-top: 4px; +} + +.hero-sub strong { + color: #ffffff; + font-weight: 700; +} + +.install-chip { + display: inline-flex; + align-items: center; + gap: 10px; + margin-top: var(--space-content); + padding: 9px 9px 9px 16px; + background: var(--code-bg); + border: 1px solid var(--surface-border); + border-radius: 10px; + max-width: 100%; +} + +.install-chip code { + font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; + font-size: 0.98rem; + color: #e6ebff; + white-space: nowrap; + overflow-x: auto; +} + +.install-chip code::before { + content: "$ "; + color: var(--accent-text); +} + +.install-hint { + margin-top: 12px; + font-family: "Ruda", sans-serif; + font-size: 0.82rem; + letter-spacing: 0.02em; + color: var(--ink-faint); +} + +.hero-ctas { + margin-top: var(--space-content); + justify-content: flex-start; +} + +.hero-microproof { + margin-top: 18px; + font-family: "Ruda", sans-serif; + font-size: 0.82rem; + letter-spacing: 0.04em; + color: rgba(255, 255, 255, 0.55); +} + +.hero-microproof strong { + color: var(--accent-text); +} + +/* ====================== SHARED SECTION HEADINGS ========================== */ +/* Centered heading treatment for the new sections (kept distinct from + main.css's left-aligned .section-title, which the legacy split sections + still use). */ +.sec-head { + text-align: center; + max-width: 760px; + margin: 0 auto clamp(1.6rem, 4vw, 2.6rem); +} + +.sec-eyebrow { + font-family: "Ruda", sans-serif; + font-size: 12.5px; + font-weight: 700; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--accent-text); + margin-bottom: 10px; +} + +.sec-eyebrow.is-teal { + color: var(--accent-teal); +} + +.sec-title { + font-family: "Rubik", sans-serif; + font-size: clamp(1.5rem, 3.6vw, 2.2rem); + font-weight: 700; + color: #ffffff; + letter-spacing: 0.01em; +} + +.sec-title::after { + content: ""; + display: block; + width: 54px; + height: 3px; + margin: 14px auto 0; + border-radius: 3px; + background: linear-gradient( + 130deg, + var(--accent) 0%, + var(--accent-soft) 100% + ); +} + +.sec-lede { + font-family: "Open Sans", sans-serif; + font-size: clamp(0.95rem, 2vw, 1.08rem); + color: var(--ink-soft); + line-height: 1.65; + margin-top: 16px; +} + +.sec-cta { + text-align: center; + margin-top: clamp(1.8rem, 4vw, 2.6rem); +} + +.text-link { + font-family: "Ruda", sans-serif; + font-weight: 700; + font-size: 0.95rem; + color: var(--accent-text); + text-decoration: none; + border-bottom: 1px solid rgba(255, 195, 121, 0.35); + transition: color 0.2s ease; +} + +.text-link:hover, +.text-link:focus-visible { + color: var(--accent-strong); +} + +/* =========================== STATS BAND ================================= */ +.stats-band { + padding: clamp(1.6rem, 4vw, 2.3rem) clamp(1.5rem, 4vw, 2.4rem); +} + +.stat-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: clamp(1.1rem, 4vw, 3rem); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 88px; +} + +.stat-num { + font-family: "Rubik", sans-serif; + font-size: clamp(1.5rem, 4vw, 2.15rem); + font-weight: 700; + color: #ffffff; + line-height: 1.1; +} + +.stat-num.is-accent { + color: var(--accent-text); +} + +.stat-label { + font-family: "Ruda", sans-serif; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ink-faint); + margin-top: 6px; + text-align: center; +} + +.stats-note { + margin-top: 18px; + text-align: center; + font-family: "Open Sans", sans-serif; + font-size: 0.82rem; + color: var(--ink-faint); +} + +/* ===================== QUICKSTART / CODE BLOCK ========================== */ +.quickstart-layout { + align-items: start; +} + +.qs-steps { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 18px; +} + +.qs-step { + display: grid; + grid-template-columns: 40px 1fr; + gap: 14px; + align-items: start; +} + +.qs-num { + width: 40px; + height: 40px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + font-family: "Rubik", sans-serif; + font-weight: 700; + color: var(--accent-text); + background: rgba(240, 171, 80, 0.12); + border: 1px solid rgba(240, 171, 80, 0.28); +} + +.qs-step h3 { + font-family: "Rubik", sans-serif; + font-size: 1.02rem; + color: #ffffff; + margin: 6px 0 2px; +} + +.qs-step p { + font-family: "Open Sans", sans-serif; + font-size: 0.92rem; + line-height: 1.55; + color: var(--ink-soft); +} + +.qs-step code { + font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; + color: var(--accent-teal-soft); + font-size: 0.88em; +} + +.code-block { + position: relative; + background: var(--code-bg); + border: 1px solid var(--surface-border); + border-radius: 14px; + padding: 44px 16px 16px; + overflow-x: auto; +} + +.code-block::before { + content: ""; + position: absolute; + top: 16px; + left: 16px; + width: 42px; + height: 10px; + border-radius: 999px; + background: radial-gradient(circle at 5px 5px, #ff5f57 4px, transparent 5px), + radial-gradient(circle at 21px 5px, #febc2e 4px, transparent 5px), + radial-gradient(circle at 37px 5px, #28c840 4px, transparent 5px); +} + +.code-block code { + display: block; + font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; + font-size: 0.86rem; + line-height: 1.7; + color: #e6ebff; + white-space: pre; + tab-size: 4; +} + +.tok-kw { + color: #ff9ec4; +} +.tok-cls { + color: #7fd4ff; +} +.tok-fn { + color: #8ce6b0; +} +.tok-str { + color: #ffd38f; +} +.tok-num { + color: #c3a6ff; +} +.tok-com { + color: rgba(255, 255, 255, 0.5); + font-style: italic; +} +.tok-op { + color: rgba(255, 255, 255, 0.72); +} + +.copy-btn { + position: absolute; + top: 10px; + right: 10px; + display: inline-flex; + align-items: center; + gap: 6px; + font-family: "Ruda", sans-serif; + font-size: 12px; + font-weight: 700; + color: rgba(255, 255, 255, 0.82); + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--surface-border); + border-radius: 8px; + padding: 6px 10px; + cursor: pointer; + transition: + color 0.2s ease, + border-color 0.2s ease, + background 0.2s ease; +} + +.copy-btn:hover, +.copy-btn:focus-visible { + color: #ffffff; + background: rgba(255, 255, 255, 0.12); +} + +.copy-btn.is-copied { + color: var(--accent-teal); + border-color: rgba(45, 212, 191, 0.5); +} + +.install-chip .copy-btn { + position: static; +} + +.qs-ctas { + margin-top: clamp(1.6rem, 4vw, 2.2rem); +} + +/* ============================ SHOWCASE ================================== */ +.showcase-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(258px, 1fr)); + gap: 20px; +} + +.showcase-card { + background: var(--panel); + border: 1px solid var(--hairline); + border-radius: 18px; + overflow: hidden; + transition: + transform 0.3s ease, + border-color 0.3s ease, + box-shadow 0.3s ease; +} + +.showcase-card:hover { + transform: translateY(-6px); + border-color: var(--hairline-strong); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.42); +} + +.showcase-figure { + padding: 12px 12px 0; + background: radial-gradient( + circle at 50% 35%, + rgba(240, 171, 80, 0.08), + transparent 70% + ); +} + +.showcase-figure img { + display: block; + width: 100%; + height: auto; +} + +.showcase-cap { + padding: 10px 20px 22px; +} + +.showcase-cap h3 { + font-family: "Rubik", sans-serif; + font-size: 1.04rem; + color: #ffffff; + margin-bottom: 5px; +} + +.showcase-cap p { + font-family: "Open Sans", sans-serif; + font-size: 0.88rem; + line-height: 1.5; + color: var(--ink-soft); +} + +/* ========================= FEATURES GRID ================================ */ +/* Auto-scrolling feature carousel (driven by the marquee script in + index.html). Bleeds to the panel edges; the section's overflow:hidden clips + the scrolling track at the rounded corners. */ +.feat-marquee { + margin: 0 calc(-1 * clamp(1.5rem, 4vw, 2.4rem)); + padding: 10px clamp(1.5rem, 4vw, 2.4rem); + overflow-x: auto; + overflow-y: hidden; +} + +/* Scrollbar hidden only while the auto-scroll is active (JS adds the class); + without JS the scrollbar stays as a manual-scroll affordance. */ +.feat-marquee.is-animating { + scrollbar-width: none; +} + +.feat-marquee.is-animating::-webkit-scrollbar { + display: none; +} + +.marquee-track { + display: flex; + gap: 18px; + width: max-content; +} + +.feat-marquee .feat-card { + flex: 0 0 clamp(240px, 72vw, 288px); +} + +.feat-card { + background: var(--panel); + border: 1px solid var(--hairline); + border-radius: 16px; + padding: 24px 22px; + transition: + transform 0.3s ease, + background 0.3s ease, + border-color 0.3s ease, + box-shadow 0.3s ease; +} + +.feat-card:hover { + transform: translateY(-6px); + background: var(--panel-strong); + border-color: var(--hairline-strong); + box-shadow: 0 16px 36px rgba(0, 0, 0, 0.36); +} + +.feat-icon { + width: 46px; + height: 46px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + font-size: 20px; + margin-bottom: 16px; + color: var(--accent-text); + background: rgba(240, 171, 80, 0.12); + border: 1px solid rgba(240, 171, 80, 0.24); +} + +.feat-card h3 { + font-family: "Rubik", sans-serif; + font-size: 1.06rem; + color: #ffffff; + margin-bottom: 8px; +} + +.feat-card p { + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + line-height: 1.55; + color: var(--ink-soft); +} + +/* ==================== VALIDATION + USED-BY WALL ========================= */ +.validation-layout { + align-items: center; + margin-bottom: clamp(1.8rem, 4vw, 2.6rem); +} + +.validation-head .big-stat { + font-family: "Rubik", sans-serif; + font-size: clamp(2.4rem, 7vw, 3.6rem); + font-weight: 700; + color: var(--accent-text); + line-height: 1; +} + +.validation-head p { + font-family: "Open Sans", sans-serif; + font-size: 1rem; + line-height: 1.65; + color: var(--ink-soft); + margin-top: 12px; +} + +.usedby-label { + text-align: center; + font-family: "Ruda", sans-serif; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--ink-faint); + margin-bottom: 18px; +} + +.inst-grid { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(228px, 1fr)); + gap: 14px; +} + +.inst-card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 18px; + border-radius: 14px; + background: rgba(15, 23, 42, 0.35); + border: 1px solid var(--hairline); + border-left: 3px solid var(--accent); + transition: + transform 0.25s ease, + background 0.25s ease; +} + +.inst-card:hover { + transform: translateY(-3px); + background: rgba(33, 35, 50, 0.55); +} + +.inst-name { + font-family: "Rubik", sans-serif; + font-size: 0.98rem; + font-weight: 700; + color: #ffffff; +} + +.inst-place { + font-family: "Open Sans", sans-serif; + font-size: 0.84rem; + color: var(--ink-faint); +} + +.contrib-note { + font-family: "Open Sans", sans-serif; + font-size: 0.86rem; + color: var(--ink-faint); + text-align: center; + max-width: 640px; + margin: 1.8rem auto 0; + line-height: 1.6; +} + +.contrib-note a { + color: var(--accent-soft); + text-decoration: none; + border-bottom: 1px solid rgba(255, 211, 143, 0.35); +} + +/* =========================== ECOSYSTEM ================================== */ +.eco-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 18px; +} + +.eco-card { + position: relative; + display: flex; + flex-direction: column; + padding: 26px 24px; + border-radius: 16px; + background: var(--panel); + border: 1px solid var(--hairline); + overflow: hidden; + text-decoration: none; + transition: + transform 0.3s ease, + background 0.3s ease, + border-color 0.3s ease, + box-shadow 0.3s ease; +} + +.eco-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + opacity: 0.85; + background: linear-gradient(90deg, var(--accent) 0%, var(--accent-soft) 100%); +} + +.eco-card:hover { + transform: translateY(-6px); + background: var(--panel-strong); + border-color: var(--hairline-strong); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.4); +} + +.eco-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.eco-icon { + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 11px; + font-size: 18px; + color: var(--accent-text); + background: rgba(240, 171, 80, 0.12); + border: 1px solid rgba(240, 171, 80, 0.24); + flex-shrink: 0; +} + +.eco-name { + font-family: "Rubik", sans-serif; + font-weight: 700; + font-size: 1.1rem; + color: #ffffff; +} + +.eco-badge { + margin-left: auto; + font-family: "Ruda", sans-serif; + font-size: 12px; + font-weight: 700; + color: var(--ink-faint); + white-space: nowrap; +} + +.eco-badge .fa-star { + color: var(--accent-strong); + font-size: 11px; +} + +.eco-desc { + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + line-height: 1.55; + color: var(--ink-soft); + flex-grow: 1; +} + +.eco-link { + margin-top: 16px; + font-family: "Ruda", sans-serif; + font-weight: 700; + font-size: 13px; + color: var(--accent-text); +} + +.eco-card:hover .eco-link { + color: var(--accent-strong); +} + +/* ============================ R&D BAND ================================== */ +.rnd-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 18px; +} + +.rnd-card { + padding: 24px 22px; + border-radius: 16px; + background: rgba(15, 23, 42, 0.4); + border: 1px solid var(--hairline); + border-left: 3px solid var(--accent-teal); + transition: + transform 0.3s ease, + background 0.3s ease; +} + +.rnd-card:hover { + transform: translateY(-5px); + background: rgba(33, 35, 50, 0.55); +} + +.rnd-icon { + width: 44px; + height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + font-size: 19px; + margin-bottom: 15px; + color: var(--accent-teal); + background: rgba(45, 212, 191, 0.12); + border: 1px solid rgba(45, 212, 191, 0.28); +} + +.rnd-tag { + display: inline-block; + font-family: "Ruda", sans-serif; + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent-teal); + margin-bottom: 8px; +} + +.rnd-card h3 { + font-family: "Rubik", sans-serif; + font-size: 1.05rem; + color: #ffffff; + margin-bottom: 8px; +} + +.rnd-card p { + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + line-height: 1.55; + color: var(--ink-soft); +} + +/* =========================== COMMUNITY ================================= */ +.community-lede { + max-width: 720px; + margin: 0 auto clamp(1.6rem, 4vw, 2.2rem); + text-align: center; +} + +.community-lede p { + font-family: "Open Sans", sans-serif; + font-size: 1rem; + line-height: 1.7; + color: var(--ink-soft); +} + +.community-lede strong { + color: #ffffff; +} + +.community-lede a { + color: var(--accent-soft); + text-decoration: none; + border-bottom: 1px solid rgba(255, 211, 143, 0.35); +} + +.channels { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); + gap: 14px; + margin-bottom: clamp(1.8rem, 4vw, 2.4rem); +} + +.channel-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 20px 12px; + border-radius: 14px; + background: rgba(15, 23, 42, 0.35); + border: 1px solid var(--hairline); + text-decoration: none; + transition: + transform 0.25s ease, + background 0.25s ease, + border-color 0.25s ease; +} + +.channel-card i { + font-size: 26px; + color: #ffffff; +} + +.channel-card span { + font-family: "Ruda", sans-serif; + font-weight: 700; + font-size: 13px; + color: var(--ink-soft); +} + +.channel-card:hover { + transform: translateY(-4px); + background: rgba(33, 35, 50, 0.6); + border-color: rgba(240, 171, 80, 0.35); +} + +.channel-card:hover span { + color: #ffffff; +} + +/* ============================ CITE ===================================== */ +.cite-grid { + display: grid; + gap: 16px; + max-width: 920px; + margin: 0 auto; +} + +.cite-item { + background: var(--panel); + border: 1px solid var(--hairline); + border-left: 3px solid var(--accent); + border-radius: 14px; + padding: 20px 22px; +} + +.cite-item h3 { + font-family: "Rubik", sans-serif; + font-size: 1rem; + color: #ffffff; + margin-bottom: 6px; +} + +.cite-meta { + font-family: "Open Sans", sans-serif; + color: var(--ink-soft); + font-size: 0.9rem; + line-height: 1.55; +} + +.cite-meta a { + color: var(--accent-text); + text-decoration: none; + border-bottom: 1px solid rgba(255, 195, 121, 0.35); +} + +.cite-bibtex { + max-width: 920px; + margin: 20px auto 0; +} + +.cite-bibtex .code-block { + padding-top: 16px; +} + +.cite-bibtex .code-block::before { + content: none; +} + +/* ============================ FINAL CTA ================================= */ +.final-cta { + text-align: center; + padding: clamp(2.2rem, 6vw, 3.6rem) 1.5rem; +} + +.final-cta .install-chip { + margin: var(--space-content) auto 0; +} + +.final-cta .cta-buttons { + margin-top: var(--space-content); +} + +/* ===================== PORTED CTA CLUSTER (about.css) =================== */ +.cta-buttons { + display: flex; + flex-wrap: wrap; + gap: 14px; + justify-content: center; +} + +.cta-btn { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: "Ruda", sans-serif; + font-size: 15px; + font-weight: 700; + padding: 13px 26px; + border-radius: 999px; + text-decoration: none; + transition: + transform 0.2s ease, + box-shadow 0.2s ease, + background 0.2s ease; +} + +.cta-primary { + background: linear-gradient( + 130deg, + var(--accent) 0%, + var(--accent-soft) 100% + ); + color: #030b28; + box-shadow: 0 6px 20px rgba(240, 171, 80, 0.35); +} + +.cta-primary:hover, +.cta-primary:focus-visible { + transform: translateY(-2px); + box-shadow: 0 10px 26px rgba(240, 171, 80, 0.5); +} + +.cta-secondary { + background: rgba(255, 255, 255, 0.06); + color: #ffffff; + border: 1px solid var(--hairline-strong); +} + +.cta-secondary:hover, +.cta-secondary:focus-visible { + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.12); +} + +.cta-ghost { + background: transparent; + color: var(--ink-soft); + border: 1px solid transparent; +} + +.cta-ghost:hover, +.cta-ghost:focus-visible { + color: #ffffff; +} + +/* ===================== EXTENDED SCROLL-REVEAL STAGGER =================== */ +/* main.css only staggers children 1–4; grids need more. */ +.reveal-section.is-visible .reveal-item:nth-child(5) { + transition-delay: 0.36s; +} +.reveal-section.is-visible .reveal-item:nth-child(6) { + transition-delay: 0.42s; +} +.reveal-section.is-visible .reveal-item:nth-child(7) { + transition-delay: 0.48s; +} +.reveal-section.is-visible .reveal-item:nth-child(8) { + transition-delay: 0.52s; +} +.reveal-section.is-visible .reveal-item:nth-child(9) { + transition-delay: 0.56s; +} +.reveal-section.is-visible .reveal-item:nth-child(n + 10) { + transition-delay: 0.6s; +} + +/* ============================ RESPONSIVE =============================== */ +@media (max-width: 900px) { + .nav-links { + display: none; + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + gap: 4px; + padding: 12px; + margin: 0; + border-radius: 14px; + background: rgba(6, 13, 38, 0.96); + border: 1px solid var(--surface-border); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); + -webkit-backdrop-filter: blur(14px); + backdrop-filter: blur(14px); + } + + .site-header.nav-open .nav-links { + display: flex; + } + + .nav-link { + padding: 12px 14px; + } + + .nav-toggle { + display: inline-flex; + } + + .site-header { + position: relative; + top: 0; + flex-wrap: wrap; + } +} + +@media (max-width: 560px) { + .star-badge { + display: none; + } + + .site-header .brand-logo { + width: 150px; + height: 34px; + } + + .nav-cta span:not(.simulate-icon) { + display: inline; + } + + .qs-ctas .cta-btn, + .hero-ctas .cta-btn { + width: 100%; + justify-content: center; + } +} + +/* Legacy centered heading collision guard: the new sections use .sec-title, + so main.css's .section-title is untouched here. */ + +/* ========================= REDUCED MOTION ============================== */ +@media (prefers-reduced-motion: reduce) { + .site-header, + .star-badge, + .nav-link, + .cta-btn, + .copy-btn, + .feat-card, + .eco-card, + .rnd-card, + .inst-card, + .showcase-card, + .channel-card { + transition: none; + } + + .cta-btn:hover, + .feat-card:hover, + .eco-card:hover, + .rnd-card:hover, + .inst-card:hover, + .showcase-card:hover, + .channel-card:hover, + .star-badge:hover { + transform: none; + } + + /* No auto-scroll: fall back to a static, wrapping grid of feature cards. */ + .feat-marquee { + overflow: visible; + margin: 0; + padding: 0; + } + + .marquee-track { + flex-wrap: wrap; + width: auto; + justify-content: center; + } + + .feat-marquee .feat-card { + flex: 1 1 240px; + max-width: 320px; + } +} diff --git a/css/main.css b/css/main.css index c7e9d91..4a6f1da 100644 --- a/css/main.css +++ b/css/main.css @@ -434,3 +434,80 @@ body::before { max-width: 250px; } } + +/* ---------------------- ACCESSIBILITY UTILITIES ------------------------ */ +/* Visually-hidden helper (screen-reader only). */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Keyboard "skip to content" link — hidden until focused. */ +.skip-link { + position: absolute; + left: 12px; + top: -64px; + z-index: 100; + padding: 10px 16px; + border-radius: 8px; + background: var(--accent-strong); + color: #111a3a; + font-family: "Ruda", sans-serif; + font-weight: 700; + text-decoration: none; + transition: top 0.2s ease; +} + +.skip-link:focus { + top: 12px; +} + +/* Visible focus ring for keyboard users across interactive controls. */ +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--accent-strong); + outline-offset: 2px; + border-radius: 4px; +} + +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} + +/* ------------------------- REDUCED MOTION ------------------------------ */ +/* Honour users who ask for less motion: freeze the starfield, disable the + scroll-reveal (CSS is the source of truth, so content shows even if JS + never runs), and drop hover lifts. */ +@media (prefers-reduced-motion: reduce) { + body::before { + animation: none; + } + + .reveal-section, + .reveal-item { + opacity: 1 !important; + transform: none !important; + transition: none !important; + } + + .simulate-button, + .icon-button { + transition: none; + } + + .simulate-button:hover, + .simulate-button:focus-visible, + .icon-button:hover, + .icon-button:focus-visible { + transform: none; + } +} diff --git a/images/showcase/altitude-speed.svg b/images/showcase/altitude-speed.svg new file mode 100644 index 0000000..0c653bb --- /dev/null +++ b/images/showcase/altitude-speed.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/showcase/flight-3d.svg b/images/showcase/flight-3d.svg new file mode 100644 index 0000000..94bf295 --- /dev/null +++ b/images/showcase/flight-3d.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/showcase/stability-margin.svg b/images/showcase/stability-margin.svg new file mode 100644 index 0000000..32c3ebf --- /dev/null +++ b/images/showcase/stability-margin.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html index d625f14..4d84996 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,11 @@ @@ -53,10 +53,22 @@ + + + + RocketPy - Advanced Rocket Trajectory Simulator +
-
-
- - - -
- + href="https://app.rocketpy.org/" + class="simulate-button nav-cta" + role="button" + > + + Simulate + + +
-
+
+

- Open-source aerospace simulation + Open source · 6-DOF rocket flight simulation

The sky is not the limit!

-

- RocketPy is an innovative rocket trajectory simulator that - includes features never seen before in the state-of-the-art - rocket trajectory simulators. +

+ RocketPy is the next-generation, open-source simulator for + High-Power Rocketry — a full six-degrees-of-freedom flight built + on real weather data, Monte Carlo-ready, and + validated to under 1% apogee error against real + launches. +

+
+ pip install rocketpy + +
+

+ Python ≥ 3.10 · also on conda-forge · runs in Jupyter & + Google Colab +

+ +

+ MIT licensed · + 1,021 + on GitHub · 170k+ total downloads · used on 6 continents

- - - SIMULATE WITH ROCKETPY -
+ +
+

RocketPy by the numbers

+
+
+ 1,021 + GitHub stars +
+
+ 268 + Forks +
+
+ 75+ + Contributors +
+
+ 25k+ + Downloads (6 mo) +
+
+ <1% + Apogee error +
+
+ 2018 + Building since +
+
+

+ Live from GitHub & PyPI, refreshed on every deploy — + star us on GitHub. +

+
+ + +
+
+

Quickstart

+

+ Your first flight, in four classes +

+

+ Describe the sky, the motor, and the rocket — RocketPy handles the + full 6-DOF physics. Here is a complete simulation of the + Calisto rocket, ready to run in a notebook or Google + Colab. +

+
+
+
    +
  1. + 1 +
    +

    Environment

    +

    + Pull real forecast, reanalysis or sounding data for launch + day. +

    +
    +
  2. +
  3. + 2 +
    +

    SolidMotor

    +

    + Model solid, hybrid or liquid propulsion from a thrust + curve. +

    +
    +
  4. +
  5. + 3 +
    +

    Rocket

    +

    + Add nose cone, fins, tail and parachutes with real + aerodynamics. +

    +
    +
  6. +
  7. + 4 +
    +

    Flight

    +

    Launch it and explore trajectory, forces and recovery.

    +
    +
  8. +
+
+
from rocketpy import Environment, SolidMotor, Rocket, Flight
+
+# 1 · Describe the sky — real forecast weather
+env = Environment(latitude=32.99, longitude=-106.97, elevation=1400)
+env.set_atmospheric_model(type="Forecast", file="GFS")
+
+# 2 · Build the motor
+motor = SolidMotor(
+    thrust_source="Cesaroni_M1670.eng",
+    dry_mass=1.815, grain_number=5, nozzle_radius=0.033,
+)
+
+# 3 · Assemble the rocket
+calisto = Rocket(radius=0.0635, mass=14.426, inertia=(6.32, 6.32, 0.034))
+calisto.add_motor(motor, position=-1.255)
+calisto.add_nose(length=0.558, kind="von karman", position=1.278)
+calisto.add_trapezoidal_fins(n=4, root_chord=0.12, tip_chord=0.06, span=0.11)
+calisto.add_parachute("Main", cd_s=10.0, trigger=800)
+
+# 4 · Fly it — full 6-DOF simulation
+flight = Flight(rocket=calisto, environment=env, rail_length=5.2)
+flight.all_info()
+
+
+ +
+ +
+
+

See what it produces

+

+ Publication-grade results +

+

+ One simulation yields the full picture — 3D trajectory, + kinematics, stability, and the statistical landing footprint that + matters for a safe recovery. +

+
+
+
+
+ A three-dimensional flight trajectory arcing over a ground grid +
+
+

3D trajectory

+

+ Full flight path in space — exportable to Google Earth via + KML. +

+
+
+
+
+ Monte Carlo impact dispersion with a confidence ellipse around the target +
+
+

Monte Carlo dispersion

+

+ Thousands of runs give the landing ellipse and its confidence + interval. +

+
+
+
+
+ Altitude and velocity plotted against time, peaking at apogee +
+
+

Altitude & velocity

+

Every flight variable resolved continuously versus time.

+
+
+
+
+ Static stability margin over time staying inside the stable band +
+
+

Stability margin

+

+ Track the static margin through the burn to stay in control. +

+
+
+
+

+ Explore worked examples in the docs → +

+
+ + +
-

- Let's be realistic -

-
-
-

- RocketPy's environment construction uses weather data from - several meteorological agencies, providing precise information - about atmospheric conditions and taking into account different - kinds of uncertainties associated with the mathematical models - of the phenomena. - - Read our paper on the Journal of Aerospace Engineering - - to know more about the mathematical models and the software - architecture. +

+

Capabilities

+

+ Everything a serious flight needs +

+

+ High-fidelity physics under the hood, a clean Python API on top — + from propulsion to recovery, weather to control. +

+
+
+
+
+ +

Full 6-DOF flight

+

+ Nonlinear six-degrees-of-freedom dynamics with rigorous + variable-mass effects. +

+
+
+ +

Monte Carlo & sensitivity

+

+ Dispersion analysis and global sensitivity for landing-ellipse + prediction. +

+
+
+ +

Solid, hybrid & liquid motors

+

+ Model any propulsion type straight from a thrust curve or + .eng file. +

+
+
+ +

Aerodynamic surfaces

+

+ Nose cones, fins and tails via Barrowman equations, plus + custom drag curves. +

+
+
+ +

Parachutes & recovery

+

+ Multiple chutes with custom trigger functions and sensor-noise + simulation. +

+
+
+ +

Real weather & atmosphere

+

+ ISA 1976, Wyoming soundings and live NOAA / ECMWF forecast and + ensemble data. +

+
+
+ +

Multi-stage rockets

+

+ Simulate staged vehicles with independent motors, events and + recovery. +

+
+
+ +

Custom control laws

+

+ Inject continuous or discrete control for fins, air-brakes and + active systems. +

+
+
+ +

Stability analysis

+

+ Static margin, damping and out-of-rail conditions resolved + across the flight. +

+
+
+ +

Sensor simulation

+

+ Emulate accelerometers, gyroscopes and barometers with + realistic noise. +

+
+
+ +

MATLAB & Python interop

+

+ Script it in Python or drive it from MATLAB — and export + results anywhere. +

+
+
+ +

KML / Google Earth

+

+ Export trajectories to KML and fly them over real terrain in + Google Earth. +

+
+
+
+
+ + +
+
+
+

Proven in the field

+

<1%

+

+ Apogee error versus real flights +

+

+ Because RocketPy flies on the actual sky — real forecast, + reanalysis and sounding data — its predictions match reality. It + has been validated against + 17 documented real flights from student and + professional teams across four continents.

Atmospheric layers and weather sounding data above a planet horizon />
+

+ Flown & validated by teams worldwide +

+
    +
  • + Projeto Jupiter — USP + São Paulo, Brazil +
  • +
  • + EPFL Rocket Team + Lausanne, Switzerland +
  • +
  • + University of Notre Dame + Indiana, USA +
  • +
  • + Space Enterprise at Berkeley + California, USA +
  • +
  • + U. of Toronto Aerospace + Toronto, Canada +
  • +
  • + TU Wien Space Team + Vienna, Austria +
  • +
  • + Faraday Rocketry UPV + Valencia, Spain +
  • +
  • + Politecnico di Torino + Turin, Italy +
  • +
  • + Aerospace Team Graz + Graz, Austria +
  • +
  • + STAR — UC3M + Madrid, Spain +
  • +
  • + Aristotle Space & Aeronautics + Thessaloniki, Greece +
  • +
  • + BME Suborbitals + Budapest, Hungary +
  • +
+

+ Trusted by 100+ university teams at the + Spaceport America Cup, the + European Rocketry Challenge (EuRoC) and the + Latin American Space Challenge. + See all validation examples → +

+
-

- Simulation is all about predictions -

- +
+ + +
+
+

On the horizon

+

+ The frontier of flight simulation +

+

+ What the team is building next — pushing accuracy, speed and + accessibility further. +

+
+
+
+ + Research +

AI surrogate models

+

+ Neural-network surrogates to accelerate Monte Carlo campaigns by + orders of magnitude. +

+
+
+ + Research +

Natural-language queries

+

+ RAG systems that let you ask aerospace questions and get answers + grounded in your data. +

+
+
+ + Roadmap +

Hardware-in-the-loop

+

+ A HIL validation framework to test avionics against a live + simulated flight. +

+
+
+ + Roadmap +

Built for AI agents

+

+ An MCP server so LLMs and autonomous agents can design and fly + rockets directly. +

+
+
-

- From rocketeers to rocketeers -

+
+

From rocketeers to rocketeers

+

+ Built by a global community +

+
-
-

- RocketPy is an Open Source software created by rocket - enthusiasts. Since it is open source, each and every one of you - is more than welcome to be a part of it. Check out our project - on +

+

+ RocketPy is free and MIT-licensed. It was born + inside Projeto Jupiter, the student rocketry + team at the University of São Paulo, Brazil — and grew into the + world's leading open-source trajectory simulator, maintained by + contributors on six continents. +

+

+ Everyone is welcome. Ask a question, report an idea, or ship a + pull request. + Meet the people behind RocketPy → +

+
GitHub. -

+ class="cta-btn cta-primary" + > + + Contribute on GitHub + + + + Join our Discord + +
A connected network of RocketPy contributors around a central hub />
+
-
- -
- + +
+
+

For researchers

+

Using RocketPy? Cite it

+

+ RocketPy is peer-reviewed. If it supports your research, please + cite the papers below. +

+
+
+
+

+ RocketPy: Six Degree-of-Freedom Rocket Trajectory Simulator +

+

+ Ceotto, G. H.; Schmitt, R. N.; Alves, G. F.; Pezente, L. A.; + Carmo, B. S. (2021). Journal of Aerospace Engineering, + 34(6). ASCE. + doi:10.1061/(ASCE)AS.1943-5525.0001331 +

+
+
+

RocketPy: Combining Open-Source and Scientific Libraries…

+

+ Soares et al. (2022). + Proceedings of the Python in Science Conference (SciPy). + doi:10.25080/majora-212e5952-020 +

+
+
+
+
@article{RocketPy2021,
+  author  = {Ceotto, Giovani H. and Schmitt, Rodrigo N. and
+             Alves, Guilherme F. and Pezente, Lucas A. and Carmo, Bruno S.},
+  title   = {{RocketPy}: Six Degree-of-Freedom Rocket Trajectory Simulator},
+  journal = {Journal of Aerospace Engineering},
+  volume  = {34},
+  number  = {6},
+  year    = {2021},
+  doi     = {10.1061/(ASCE)AS.1943-5525.0001331}
+}
+
+
-
+ + + diff --git a/package.json b/package.json index 6d63d09..1045730 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,13 @@ "scripts": { "format": "prettier --write '**/*.{html,css,js}'", "build": "npm run build:css && npm run build:html", - "build:css": "npm run minify:css:main && npm run minify:css:footer && npm run minify:css:about", + "build:css": "npm run minify:css:main && npm run minify:css:footer && npm run minify:css:about && npm run minify:css:home", "build:html": "npm run minify:html:index && npm run minify:html:about", + "stats": "node scripts/fetch-stats.mjs", "minify:css:main": "lightningcss --minify --bundle css/main.css -o dist/css/main.css", "minify:css:footer": "lightningcss --minify --bundle css/footer.css -o dist/css/footer.css", "minify:css:about": "lightningcss --minify --bundle css/about.css -o dist/css/about.css", + "minify:css:home": "lightningcss --minify --bundle css/home.css -o dist/css/home.css", "minify:html:index": "html-minifier --collapse-whitespace --remove-comments index.html -o dist/index.html", "minify:html:about": "html-minifier --collapse-whitespace --remove-comments about.html -o dist/about.html", "prebuild": "node -e \"const fs = require('fs'); fs.rmSync('dist', { recursive: true, force: true }); ['dist', 'dist/css', 'dist/images', 'dist/about-images'].forEach(dir => fs.mkdirSync(dir, { recursive: true }));\"", diff --git a/scripts/fetch-stats.mjs b/scripts/fetch-stats.mjs new file mode 100644 index 0000000..2894f81 --- /dev/null +++ b/scripts/fetch-stats.mjs @@ -0,0 +1,126 @@ +#!/usr/bin/env node +/** + * Build-time stats injector. + * + * Fetches live project metrics (GitHub stars/forks/contributors, recent PyPI + * downloads) and rewrites the matching `data-stat="..."` anchors inside the + * ALREADY-BUILT `dist/index.html`. It never touches the source `index.html`, + * and it is best-effort: any network/parse failure falls back to the committed + * defaults so a deploy can never be broken by a flaky API. + * + * Runs after `npm run build`, on the CI runner (see .github/workflows/static.yml), + * authenticated with GITHUB_TOKEN to avoid the 60 req/hr unauthenticated limit. + * Node 20+ (global fetch, no dependencies). + */ +import { readFile, writeFile } from "node:fs/promises"; + +const REPO = "RocketPy-Team/RocketPy"; +const HTML = "dist/index.html"; + +/* Committed fallbacks — kept in sync with the defaults hard-coded in index.html + so the page still reads sensibly if every request below fails. `downloads` + is a rolling ~180-day figure (see fetchDownloads), not the all-time total. */ +const stats = { stars: 1021, forks: 268, contributors: 75, downloads: 25000 }; + +const gh = (path) => + fetch(`https://api.github.com/${path}`, { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "rocketpy-site-build", + ...(process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}), + }, + }); + +/** Compact human formatting: 1021 -> "1,021", 173210 -> "173k+". */ +const comma = (n) => n.toLocaleString("en-US"); +const kplus = (n) => + n >= 1000 ? Math.floor(n / 1000).toLocaleString("en-US") + "k+" : String(n); + +/** Total contributors via the Link header's last-page number (per_page=1). */ +async function fetchContributors() { + const res = await gh(`repos/${REPO}/contributors?per_page=1&anon=true`); + if (!res.ok) throw new Error(`contributors HTTP ${res.status}`); + const link = res.headers.get("link") || ""; + const m = link.match(/[?&]page=(\d+)>;\s*rel="last"/); + if (m) return Number(m[1]); + // No pagination -> everyone fit on one page; count the array. + const arr = await res.json(); + return Array.isArray(arr) ? arr.length : null; +} + +/** Rolling PyPI downloads over the window pypistats exposes (~180 days) = + sum of the non-mirror daily series. Not the all-time total (pypistats + only keeps ~180 days of history). */ +async function fetchDownloads() { + const res = await fetch( + "https://pypistats.org/api/packages/rocketpy/overall?mirrors=false", + { headers: { "User-Agent": "rocketpy-site-build" } }, + ); + if (!res.ok) throw new Error(`pypistats HTTP ${res.status}`); + const json = await res.json(); + const rows = json?.data; + if (!Array.isArray(rows) || !rows.length) throw new Error("pypistats empty"); + return rows.reduce((sum, r) => sum + (r.downloads || 0), 0); +} + +/* --- gather (each source isolated; one failure keeps the others + fallback) --- */ +try { + const res = await gh(`repos/${REPO}`); + if (!res.ok) throw new Error(`repo HTTP ${res.status}`); + const repo = await res.json(); + if (repo.stargazers_count) stats.stars = repo.stargazers_count; + if (repo.forks_count) stats.forks = repo.forks_count; +} catch (e) { + console.warn("stats: github repo ->", e.message); +} + +try { + const c = await fetchContributors(); + if (c) stats.contributors = c; +} catch (e) { + console.warn("stats: contributors ->", e.message); +} + +try { + const d = await fetchDownloads(); + if (d) stats.downloads = d; +} catch (e) { + console.warn("stats: pypi downloads ->", e.message); +} + +/* --- inject into dist/index.html on the stable data-stat anchors --- */ +const values = { + stars: comma(stats.stars), + forks: comma(stats.forks), + contributors: stats.contributors + "+", // shown as a raw "N+", e.g. "75+" + downloads: kplus(stats.downloads), +}; + +let html; +try { + html = await readFile(HTML, "utf8"); +} catch (e) { + console.warn(`stats: cannot read ${HTML} (${e.message}); skipping injection`); + process.exit(0); +} + +let hits = 0; +for (const [key, value] of Object.entries(values)) { + const re = new RegExp(`(<[^>]*\\bdata-stat="${key}"[^>]*>)([^<]*)( { + hits += 1; + return open + value + close; + }); +} + +if (hits === 0) { + console.warn( + "stats: WARNING — no data-stat anchors matched; leaving committed defaults", + ); +} else { + await writeFile(HTML, html); +} + +console.log(`stats: injected ${hits} value(s)`, values); diff --git a/sitemap.xml b/sitemap.xml index c4921aa..21c27f4 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,13 +2,13 @@ https://rocketpy.org/ - 2025-12-04 + 2026-07-19 monthly 1.0 https://rocketpy.org/about.html - 2025-12-04 + 2026-07-19 monthly 0.8 From 28bac4bf7c3df45d323595782a795ebea9521002 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Tue, 21 Jul 2026 22:45:42 -0300 Subject: [PATCH 2/4] chore(format): include .mjs files in prettier glob Ensures scripts/fetch-stats.mjs is covered by `npm run format`, keeping the build script consistent with the rest of the repo (addresses Copilot review feedback on PR #23). Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1045730..871c4a1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "RocketPy landing page - Advanced rocket trajectory simulator", "scripts": { - "format": "prettier --write '**/*.{html,css,js}'", + "format": "prettier --write '**/*.{html,css,js,mjs}'", "build": "npm run build:css && npm run build:html", "build:css": "npm run minify:css:main && npm run minify:css:footer && npm run minify:css:about && npm run minify:css:home", "build:html": "npm run minify:html:index && npm run minify:html:about", From 6fafcbda51f625de25953ac5b4529f83c0c505e4 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Tue, 21 Jul 2026 22:53:25 -0300 Subject: [PATCH 3/4] fix(stats): cap network requests with a timeout so a hung API can't stall deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `continue-on-error` handles a failed stats fetch but not a hung one — a slow or stalled GitHub/PyPI endpoint would keep the deploy step waiting up to the job's default timeout. Add an 8s AbortSignal.timeout to both fetches (rejection falls through to the existing try/catch and committed defaults) and a 3-minute step-level timeout-minutes as a backstop. Addresses the blocking finding from the PR #23 review. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/static.yml | 1 + scripts/fetch-stats.mjs | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 84f49e0..37d5d77 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -51,6 +51,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true + timeout-minutes: 3 - name: Add .nojekyll run: touch dist/.nojekyll - name: Setup Pages diff --git a/scripts/fetch-stats.mjs b/scripts/fetch-stats.mjs index 2894f81..932b125 100644 --- a/scripts/fetch-stats.mjs +++ b/scripts/fetch-stats.mjs @@ -17,6 +17,12 @@ import { readFile, writeFile } from "node:fs/promises"; const REPO = "RocketPy-Team/RocketPy"; const HTML = "dist/index.html"; +/* Hard cap per request. `continue-on-error` in CI handles a *failed* fetch, but + not a *hung* one — without this a slow/stalled API would keep the deploy step + waiting up to the job's default timeout. AbortSignal.timeout rejects the fetch + so the surrounding try/catch falls back to committed defaults. */ +const FETCH_TIMEOUT_MS = 8000; + /* Committed fallbacks — kept in sync with the defaults hard-coded in index.html so the page still reads sensibly if every request below fails. `downloads` is a rolling ~180-day figure (see fetchDownloads), not the all-time total. */ @@ -24,6 +30,7 @@ const stats = { stars: 1021, forks: 268, contributors: 75, downloads: 25000 }; const gh = (path) => fetch(`https://api.github.com/${path}`, { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { Accept: "application/vnd.github+json", "User-Agent": "rocketpy-site-build", @@ -56,7 +63,10 @@ async function fetchContributors() { async function fetchDownloads() { const res = await fetch( "https://pypistats.org/api/packages/rocketpy/overall?mirrors=false", - { headers: { "User-Agent": "rocketpy-site-build" } }, + { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + headers: { "User-Agent": "rocketpy-site-build" }, + }, ); if (!res.ok) throw new Error(`pypistats HTTP ${res.status}`); const json = await res.json(); From 8802344dc1ee632324cfce13075066eb845fb8f9 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Tue, 21 Jul 2026 23:35:51 -0300 Subject: [PATCH 4/4] feat(landing): refine copy, sync About nav, drop fabricated roadmap Landing-page content and consistency fixes from review feedback: - Hero: balance the tagline so "The sky is not the limit!" no longer orphans "limit!" on its own line (text-wrap: balance). - Quickstart: clarify that SolidMotor is one of several motor variants (Solid/Hybrid/Liquid/Generic); keep the four-class framing. - Stats band: replace "refreshed on every deploy" with a build-injected "Updated " label. - Remove the fabricated "frontier of flight simulation" R&D section (and its now-dead CSS) - the team is not working on those items. - Rename the web app to "Jarvis" in the ecosystem card and footer. - State that RocketPy is the official simulator of EuRoC (2022) and LASC (2025), supported by the team. About page now shares the landing's sticky header: - Extract the site-header/nav into a shared css/nav.css loaded by both pages (removed the duplicate block from home.css). - About uses the same nav (links back to landing sections), star badge, Simulate CTA, mobile toggle, skip link and #main target. - fetch-stats.mjs now injects the live star count into about.html too. Co-Authored-By: Claude Opus 4.8 (1M context) --- about.html | 89 +++++++++------ css/home.css | 241 +--------------------------------------- css/main.css | 3 + css/nav.css | 188 +++++++++++++++++++++++++++++++ index.html | 97 ++++------------ package.json | 3 +- scripts/fetch-stats.mjs | 70 +++++++----- 7 files changed, 322 insertions(+), 369 deletions(-) create mode 100644 css/nav.css diff --git a/about.html b/about.html index eefe8ba..fbe72dc 100644 --- a/about.html +++ b/about.html @@ -45,6 +45,7 @@ /> + @@ -53,45 +54,55 @@ +
-
-
- - - -
- + href="https://app.rocketpy.org/" + class="simulate-button nav-cta" + role="button" + > + + Simulate + + +
-
+

Since 2018 · Open Source

@@ -1055,6 +1066,22 @@ renderFeatured("coreGrid", "core"); renderContributors(); renderStats(); + + // Mobile nav toggle — mirrors the landing page's shared site-header. + const header = document.querySelector(".site-header"); + const navToggle = document.querySelector(".nav-toggle"); + if (header && navToggle) { + navToggle.addEventListener("click", () => { + const open = header.classList.toggle("nav-open"); + navToggle.setAttribute("aria-expanded", open ? "true" : "false"); + }); + header.querySelectorAll(".nav-link").forEach((link) => + link.addEventListener("click", () => { + header.classList.remove("nav-open"); + navToggle.setAttribute("aria-expanded", "false"); + }), + ); + } diff --git a/css/home.css b/css/home.css index f5ea16d..fc71020 100644 --- a/css/home.css +++ b/css/home.css @@ -50,121 +50,9 @@ section[id], max-width: 100%; } -/* ============================ STICKY HEADER ============================== */ -.site-header { - position: sticky; - top: 8px; - z-index: 50; - display: flex; - align-items: center; - gap: 16px; - padding: 10px 12px 10px 18px; - margin-bottom: var(--space-section); - border-radius: 14px; - background: rgba(6, 13, 38, 0.72); - border: 1px solid var(--surface-border); - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.32); - -webkit-backdrop-filter: blur(14px); - backdrop-filter: blur(14px); -} - -.site-header .brand-link { - display: inline-flex; - flex-shrink: 0; -} - -.site-header .brand-logo { - height: 40px; - width: 184px; -} - -.nav-links { - display: flex; - align-items: center; - gap: 2px; - margin: 0 auto 0 8px; -} - -.nav-link { - font-family: "Ruda", sans-serif; - font-size: 14px; - font-weight: 700; - letter-spacing: 0.02em; - color: rgba(255, 255, 255, 0.82); - text-decoration: none; - padding: 8px 13px; - border-radius: 8px; - transition: - color 0.2s ease, - background 0.2s ease; -} - -.nav-link:hover, -.nav-link:focus-visible { - color: #ffffff; - background: rgba(255, 255, 255, 0.07); -} - -.nav-actions { - display: flex; - align-items: center; - gap: 10px; - margin-left: auto; -} - -.star-badge { - display: inline-flex; - align-items: center; - gap: 7px; - font-family: "Ruda", sans-serif; - font-size: 13px; - font-weight: 700; - color: #ffffff; - background: var(--icon-bg); - border: 1px solid var(--surface-border); - border-radius: 999px; - padding: 7px 13px; - text-decoration: none; - white-space: nowrap; - transition: - border-color 0.2s ease, - transform 0.2s ease; -} - -.star-badge:hover, -.star-badge:focus-visible { - border-color: rgba(255, 255, 255, 0.34); - transform: translateY(-1px); -} - -.star-badge .fa-star { - color: var(--accent-strong); - font-size: 12px; -} - -.star-badge .fa-github { - font-size: 15px; -} - -.nav-cta { - min-height: 40px; - padding: 8px 18px; - font-size: 13.5px; -} - -.nav-toggle { - display: none; - align-items: center; - justify-content: center; - width: 42px; - height: 40px; - border-radius: 10px; - border: 1px solid var(--surface-border); - background: var(--icon-bg); - color: #ffffff; - font-size: 18px; - cursor: pointer; -} +/* The sticky site header / primary nav (base, responsive and reduced-motion + rules) now lives in the shared css/nav.css, loaded by both index.html and + about.html so they read as one site. */ /* =============================== HERO ==================================== */ .hero-sub { @@ -830,68 +718,6 @@ section[id], color: var(--accent-strong); } -/* ============================ R&D BAND ================================== */ -.rnd-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 18px; -} - -.rnd-card { - padding: 24px 22px; - border-radius: 16px; - background: rgba(15, 23, 42, 0.4); - border: 1px solid var(--hairline); - border-left: 3px solid var(--accent-teal); - transition: - transform 0.3s ease, - background 0.3s ease; -} - -.rnd-card:hover { - transform: translateY(-5px); - background: rgba(33, 35, 50, 0.55); -} - -.rnd-icon { - width: 44px; - height: 44px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 12px; - font-size: 19px; - margin-bottom: 15px; - color: var(--accent-teal); - background: rgba(45, 212, 191, 0.12); - border: 1px solid rgba(45, 212, 191, 0.28); -} - -.rnd-tag { - display: inline-block; - font-family: "Ruda", sans-serif; - font-size: 10.5px; - font-weight: 700; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--accent-teal); - margin-bottom: 8px; -} - -.rnd-card h3 { - font-family: "Rubik", sans-serif; - font-size: 1.05rem; - color: #ffffff; - margin-bottom: 8px; -} - -.rnd-card p { - font-family: "Open Sans", sans-serif; - font-size: 0.9rem; - line-height: 1.55; - color: var(--ink-soft); -} - /* =========================== COMMUNITY ================================= */ .community-lede { max-width: 720px; @@ -1109,59 +935,8 @@ section[id], } /* ============================ RESPONSIVE =============================== */ -@media (max-width: 900px) { - .nav-links { - display: none; - position: absolute; - top: calc(100% + 8px); - left: 0; - right: 0; - flex-direction: column; - align-items: stretch; - gap: 4px; - padding: 12px; - margin: 0; - border-radius: 14px; - background: rgba(6, 13, 38, 0.96); - border: 1px solid var(--surface-border); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); - -webkit-backdrop-filter: blur(14px); - backdrop-filter: blur(14px); - } - - .site-header.nav-open .nav-links { - display: flex; - } - - .nav-link { - padding: 12px 14px; - } - - .nav-toggle { - display: inline-flex; - } - - .site-header { - position: relative; - top: 0; - flex-wrap: wrap; - } -} - +/* Header/nav breakpoints live in css/nav.css. */ @media (max-width: 560px) { - .star-badge { - display: none; - } - - .site-header .brand-logo { - width: 150px; - height: 34px; - } - - .nav-cta span:not(.simulate-icon) { - display: inline; - } - .qs-ctas .cta-btn, .hero-ctas .cta-btn { width: 100%; @@ -1174,14 +949,10 @@ section[id], /* ========================= REDUCED MOTION ============================== */ @media (prefers-reduced-motion: reduce) { - .site-header, - .star-badge, - .nav-link, .cta-btn, .copy-btn, .feat-card, .eco-card, - .rnd-card, .inst-card, .showcase-card, .channel-card { @@ -1191,11 +962,9 @@ section[id], .cta-btn:hover, .feat-card:hover, .eco-card:hover, - .rnd-card:hover, .inst-card:hover, .showcase-card:hover, - .channel-card:hover, - .star-badge:hover { + .channel-card:hover { transform: none; } diff --git a/css/main.css b/css/main.css index 4a6f1da..17ab1cb 100644 --- a/css/main.css +++ b/css/main.css @@ -240,6 +240,9 @@ body::before { .hero-tagline { font-size: clamp(1.95rem, 4.8vw, 3rem); margin-bottom: 10px; + /* Balance the lines so short words (e.g. "limit!") don't wrap alone. + Ignored gracefully by browsers without text-wrap support. */ + text-wrap: balance; } .section-title { diff --git a/css/nav.css b/css/nav.css new file mode 100644 index 0000000..d1b7273 --- /dev/null +++ b/css/nav.css @@ -0,0 +1,188 @@ +/* ============================ SITE HEADER (shared) ======================= + The sticky top navigation shared by the landing page (index.html) and the + About page (about.html) so both read as one site. Loaded by BOTH pages via + . CSS variables come from main.css :root, which + is always loaded first. */ + +.site-header { + position: sticky; + top: 8px; + z-index: 50; + display: flex; + align-items: center; + gap: 16px; + padding: 10px 12px 10px 18px; + margin-bottom: var(--space-section); + border-radius: 14px; + background: rgba(6, 13, 38, 0.72); + border: 1px solid var(--surface-border); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.32); + -webkit-backdrop-filter: blur(14px); + backdrop-filter: blur(14px); +} + +.site-header .brand-link { + display: inline-flex; + flex-shrink: 0; +} + +.site-header .brand-logo { + height: 40px; + width: 184px; +} + +.nav-links { + display: flex; + align-items: center; + gap: 2px; + margin: 0 auto 0 8px; +} + +.nav-link { + font-family: "Ruda", sans-serif; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.02em; + color: rgba(255, 255, 255, 0.82); + text-decoration: none; + padding: 8px 13px; + border-radius: 8px; + transition: + color 0.2s ease, + background 0.2s ease; +} + +.nav-link:hover, +.nav-link:focus-visible { + color: #ffffff; + background: rgba(255, 255, 255, 0.07); +} + +.nav-actions { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} + +.star-badge { + display: inline-flex; + align-items: center; + gap: 7px; + font-family: "Ruda", sans-serif; + font-size: 13px; + font-weight: 700; + color: #ffffff; + background: var(--icon-bg); + border: 1px solid var(--surface-border); + border-radius: 999px; + padding: 7px 13px; + text-decoration: none; + white-space: nowrap; + transition: + border-color 0.2s ease, + transform 0.2s ease; +} + +.star-badge:hover, +.star-badge:focus-visible { + border-color: rgba(255, 255, 255, 0.34); + transform: translateY(-1px); +} + +.star-badge .fa-star { + color: var(--accent-strong); + font-size: 12px; +} + +.star-badge .fa-github { + font-size: 15px; +} + +.nav-cta { + min-height: 40px; + padding: 8px 18px; + font-size: 13.5px; +} + +.nav-toggle { + display: none; + align-items: center; + justify-content: center; + width: 42px; + height: 40px; + border-radius: 10px; + border: 1px solid var(--surface-border); + background: var(--icon-bg); + color: #ffffff; + font-size: 18px; + cursor: pointer; +} + +/* ------------------------------ responsive ------------------------------ */ +@media (max-width: 900px) { + .nav-links { + display: none; + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + gap: 4px; + padding: 12px; + margin: 0; + border-radius: 14px; + background: rgba(6, 13, 38, 0.96); + border: 1px solid var(--surface-border); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); + -webkit-backdrop-filter: blur(14px); + backdrop-filter: blur(14px); + } + + .site-header.nav-open .nav-links { + display: flex; + } + + .nav-link { + padding: 12px 14px; + } + + .nav-toggle { + display: inline-flex; + } + + .site-header { + position: relative; + top: 0; + flex-wrap: wrap; + } +} + +@media (max-width: 560px) { + .star-badge { + display: none; + } + + .site-header .brand-logo { + width: 150px; + height: 34px; + } + + .nav-cta span:not(.simulate-icon) { + display: inline; + } +} + +/* ------------------------------ reduced motion -------------------------- */ +@media (prefers-reduced-motion: reduce) { + .site-header, + .star-badge, + .nav-link { + transition: none; + } + + .star-badge:hover { + transform: none; + } +} diff --git a/index.html b/index.html index 4d84996..30da701 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,7 @@ /> + @@ -257,12 +258,12 @@

RocketPy by the numbers

- Live from GitHub & PyPI, refreshed on every deploy — + Updated July 2026 · star us on GitHub. + >

@@ -299,10 +300,12 @@

Environment

  • 2
    -

    SolidMotor

    +

    Motor

    - Model solid, hybrid or liquid propulsion from a thrust - curve. + Pick a variant — + SolidMotor, HybridMotor, + LiquidMotor or GenericMotor — each + built from its thrust curve and geometry.

  • @@ -333,7 +336,7 @@

    Flight

    env = Environment(latitude=32.99, longitude=-106.97, elevation=1400) env.set_atmospheric_model(type="Forecast", file="GFS") -# 2 · Build the motor +# 2 · Build the motor (SolidMotor here — also Hybrid/Liquid/Generic) motor = SolidMotor( thrust_source="Cesaroni_M1670.eng", dry_mass=1.815, grain_number=5, nozzle_radius=0.033, @@ -692,10 +695,16 @@

    - Trusted by 100+ university teams at the - Spaceport America Cup, the - European Rocketry Challenge (EuRoC) and the - Latin American Space Challenge. + RocketPy is the + official flight simulator of the European Rocketry Challenge + (EuRoC) + since 2022 and of the + Latin American Space Challenge (LASC) since 2025 — + and the RocketPy team actively supports and partners with both. It + is trusted by 100+ university teams worldwide, including at the + Spaceport America Cup. See all validation examples → @@ -780,13 +789,13 @@

    - The App + Jarvis

    - Simulate in your browser — no install required. Point, - configure, launch. + RocketPy's web app — simulate in your browser, no install + required. Point, configure, launch.

    - Open the web app → + Open Jarvis → - -
    -
    -

    On the horizon

    -

    - The frontier of flight simulation -

    -

    - What the team is building next — pushing accuracy, speed and - accessibility further. -

    -
    -
    -
    - - Research -

    AI surrogate models

    -

    - Neural-network surrogates to accelerate Monte Carlo campaigns by - orders of magnitude. -

    -
    -
    - - Research -

    Natural-language queries

    -

    - RAG systems that let you ask aerospace questions and get answers - grounded in your data. -

    -
    -
    - - Roadmap -

    Hardware-in-the-loop

    -

    - A HIL validation framework to test avionics against a live - simulated flight. -

    -
    -
    - - Roadmap -

    Built for AI agents

    -

    - An MCP server so LLMs and autonomous agents can design and fly - rockets directly. -

    -
    -
    -
    -