Files
sailing-analytics/docs/index.html
T

2090 lines
106 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eclipse Azimuth Sailing Analytics - Developer Hub</title>
<meta name="description" content="Open-source platform for live visualization, analysis, and broadcasting of sailing regattas. Proven at the 2020 and 2024 Olympics.">
<link rel="icon" href="source/favicon-light.png" type="image/png" media="(prefers-color-scheme: light)">
<link rel="icon" href="source/favicon-dark.png" type="image/png" media="(prefers-color-scheme: dark)">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* ── Brand tokens ── */
:root {
--blue: #008fcc;
--blue-dark: #006fa0;
--blue-light: #e6f5fc;
--yellow: #efaa00;
--yellow-dark:#c88e00;
--dark: #333333;
--dark-mid: #444444;
--text: #212529;
--muted: #666666;
--border: #e0e0e0;
--surface: #f8f8f8;
--light: #f6f9fc;
--white: #ffffff;
--radius: 8px;
--radius-lg: 16px;
--shadow-sm: 0 2px 8px rgba(0,0,0,.08);
--shadow-md: 0 6px 24px rgba(0,0,0,.12);
--shadow-lg: 0 12px 48px rgba(0,0,0,.16);
--transition: .2s ease;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Open Sans', system-ui, sans-serif;
color: var(--text);
background: var(--white);
background-image:
radial-gradient(ellipse 340px 600px at 0% 50%, rgba(0,143,204,.06) 0%, transparent 100%),
radial-gradient(ellipse 340px 600px at 100% 50%, rgba(0,143,204,.06) 0%, transparent 100%);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
img { max-width: 100%; display: block; }
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }
.container { max-width: 1160px; margin: 0 auto; padding: 0 24px; }
.section { padding: 80px 0; }
.section--alt { background: var(--surface); }
.section--light { background: var(--light); }
.eyebrow {
font-size: .75rem; font-weight: 700; letter-spacing: .12em;
text-transform: uppercase; color: var(--blue); margin-bottom: 10px;
}
.eyebrow--yellow { color: var(--yellow-dark); }
h1, h2, h3 { line-height: 1.25; color: var(--dark); }
h1 { font-size: clamp(2rem, 4.5vw, 3.2rem); font-weight: 800; }
h2 { font-size: clamp(1.5rem, 3vw, 2.1rem); font-weight: 700; }
h3 { font-size: 1.05rem; font-weight: 700; }
p { color: var(--muted); }
.lead { font-size: 1.1rem; line-height: 1.75; color: var(--muted); }
/* ── Buttons ── */
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 11px 24px; border-radius: 100px;
font-weight: 700; font-size: .9rem; transition: var(--transition);
cursor: pointer; border: 2px solid transparent; white-space: nowrap;
}
.btn:hover { text-decoration: none; transform: translateY(-2px); }
.btn-primary { background: var(--blue); color: var(--white); box-shadow: 0 2px 12px rgba(0,143,204,.3); }
.btn-primary:hover { background: var(--blue-dark); box-shadow: 0 4px 20px rgba(0,143,204,.4); color: var(--white); }
.btn-yellow { background: var(--yellow); color: var(--white); box-shadow: 0 2px 12px rgba(239,170,0,.3); }
.btn-yellow:hover { background: var(--yellow-dark); color: var(--white); }
.btn-outline-white { border-color: rgba(255,255,255,.6); color: var(--white); }
.btn-outline-white:hover { background: rgba(255,255,255,.12); color: var(--white); }
.btn-ghost { border-color: var(--border); color: var(--dark); background: var(--white); }
.btn-ghost:hover { border-color: var(--blue); color: var(--blue); }
/* ════════════ NAV ════════════ */
.nav {
position: sticky; top: 0; z-index: 200;
background: linear-gradient(135deg, #006fa0 0%, #008fcc 45%, #0099d6 60%, #006fa0 100%);
box-shadow: 0 2px 16px rgba(0,0,0,.22);
}
.nav__inner {
display: flex; align-items: center; justify-content: space-between;
height: 68px;
}
.nav__brand img { height: 44px; width: auto; }
.nav__links {
display: flex; align-items: center; gap: 24px;
}
.nav__links a {
font-size: .875rem; font-weight: 600; color: rgba(255,255,255,.9);
transition: color var(--transition); padding: 4px 0;
border-bottom: 2px solid transparent;
}
.nav__links a:hover { color: var(--white); text-decoration: none; border-bottom-color: var(--yellow); }
.nav__cta {
margin-left: 8px; padding: 8px 18px; font-size: .85rem;
background: var(--white); border-color: var(--white); color: var(--blue-dark) !important;
font-weight: 700;
}
.nav__cta svg { color: var(--blue-dark) !important; }
.nav__cta:hover { background: rgba(255,255,255,.9) !important; color: var(--blue-dark) !important; }
/* ════════════ HERO ════════════ */
.hero {
position: relative; overflow: hidden;
min-height: 580px;
display: flex; align-items: center;
background: var(--dark);
}
.hero__bg {
position: absolute; inset: 0; overflow: hidden;
}
.hero__bg img {
width: 100%; height: 100%; object-fit: cover; object-position: center 40%;
opacity: .38;
}
.hero__content {
position: relative; z-index: 1;
padding: 80px 0;
max-width: 680px;
}
.hero__badge {
display: inline-flex; align-items: center; gap: 8px;
background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2);
border-radius: 100px; padding: 5px 14px;
font-size: .78rem; font-weight: 700; color: rgba(255,255,255,.9);
letter-spacing: .06em; text-transform: uppercase; margin-bottom: 22px;
}
.hero__badge-dot {
width: 7px; height: 7px; border-radius: 50%; background: #4caf50;
box-shadow: 0 0 0 3px rgba(76,175,80,.3);
}
.hero h1 { color: var(--white); margin-bottom: 18px; }
.hero h1 span { color: var(--yellow); }
.hero__sub { font-size: 1.1rem; color: rgba(255,255,255,.75); line-height: 1.7; margin-bottom: 36px; max-width: 560px; }
.hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }
.hero__stats {
display: flex; gap: 40px; flex-wrap: wrap;
margin-top: 52px; padding-top: 32px;
border-top: 1px solid rgba(255,255,255,.15);
}
.hero__stat-value { font-size: 2rem; font-weight: 800; color: var(--white); line-height: 1.1; }
.hero__stat-label { font-size: .78rem; color: rgba(255,255,255,.75); font-weight: 600; text-transform: uppercase; letter-spacing: .08em; margin-top: 4px; }
/* ════════════ SOLUTIONS CAROUSEL ════════════ */
.solutions__carousel-wrap {
position: relative; margin-top: 40px;
}
.solutions__track-outer {
overflow: hidden; border-radius: var(--radius-lg); padding-top: 8px;
}
.solutions__track {
display: flex; gap: 20px;
transition: transform .5s cubic-bezier(.4,0,.2,1);
cursor: grab; user-select: none;
}
.solutions__track.dragging { transition: none; cursor: grabbing; }
.sol-card {
flex: 0 0 calc(50% - 10px);
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--dark);
box-shadow: var(--shadow-md);
position: relative;
display: flex; flex-direction: column;
transition: transform .25s, box-shadow .25s;
}
.sol-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.sol-card__img {
width: 100%; aspect-ratio: 16/10; overflow: hidden;
background: #1a2a3a; position: relative;
}
.sol-card__img img {
width: 100%; height: 100%; object-fit: cover;
transition: transform .4s ease;
display: block;
}
.sol-card:hover .sol-card__img img { transform: scale(1.04); }
.sol-card__body {
padding: 22px 24px 24px;
background: linear-gradient(160deg, #1c2e3e 0%, #0d1f2d 100%);
flex: 1; display: flex; flex-direction: column;
}
.sol-card__tag {
display: inline-block;
font-size: .68rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
padding: 3px 9px; border-radius: 100px; margin-bottom: 10px;
border: 1px solid;
}
.sol-card__tag--web { color: var(--blue); border-color: rgba(0,143,204,.4); background: rgba(0,143,204,.12); }
.sol-card__tag--android{ color: #66bb6a; border-color: rgba(102,187,106,.4); background: rgba(102,187,106,.1); }
.sol-card__tag--ios { color: #ce93d8; border-color: rgba(206,147,216,.4); background: rgba(206,147,216,.1); }
.sol-card h3 { font-size: 1.1rem; color: #fff; margin-bottom: 8px; }
.sol-card p { font-size: .85rem; color: rgba(255,255,255,.6); line-height: 1.65; flex: 1; margin-bottom: 18px; }
.sol-card__links { display: flex; gap: 8px; flex-wrap: wrap; }
.sol-card__links .btn { padding: 7px 16px; font-size: .8rem; }
.sol-card__links .btn-ghost {
border-color: rgba(255,255,255,.2); color: rgba(255,255,255,.8); background: transparent;
}
.sol-card__links .btn-ghost:hover { border-color: var(--blue); color: var(--blue); }
.sol-card__store {
display: inline-flex; align-items: center; gap: 5px;
padding: 7px 14px; border-radius: 100px;
background: transparent; border: 1.5px solid rgba(255,255,255,.22);
color: rgba(255,255,255,.7); font-size: .78rem; font-weight: 600;
transition: border-color .15s, color .15s; white-space: nowrap;
}
.sol-card__store:hover { border-color: rgba(255,255,255,.55); color: #fff; text-decoration: none; }
.sol-card__store svg { flex-shrink: 0; opacity: .8; }
.solutions__nav {
display: flex; align-items: center; justify-content: center;
gap: 16px; margin-top: 28px;
}
.solutions__arrow {
width: 40px; height: 40px; border-radius: 50%; border: 2px solid var(--border);
background: var(--white); cursor: pointer; display: flex; align-items: center;
justify-content: center; font-size: 1.1rem; transition: border-color .18s, background .18s;
color: var(--dark);
}
.solutions__arrow:hover { border-color: var(--blue); background: var(--blue-light); color: var(--blue); }
.solutions__arrow:disabled { opacity: .35; cursor: default; pointer-events: none; }
.solutions__dots { display: flex; gap: 7px; }
.solutions__dot {
width: 8px; height: 8px; border-radius: 50%; border: none; padding: 0; cursor: pointer;
background: var(--border); transition: background .2s, transform .2s;
}
.solutions__dot.active { background: var(--blue); transform: scale(1.25); }
/* ════════════ MODAL ════════════ */
.modal-overlay {
display: none; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.5);
align-items: center; justify-content: center;
padding: 20px;
}
.modal-overlay.open { display: flex; }
.modal {
background: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
width: 100%; max-width: 540px;
overflow: hidden;
animation: modalIn .2s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: translateY(16px) scale(.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.modal__header {
background: linear-gradient(135deg, #006fa0 0%, #008fcc 100%);
color: var(--white);
padding: 20px 24px;
display: flex; align-items: center; justify-content: space-between;
}
.modal__header-left { display: flex; flex-direction: column; gap: 4px; }
.modal__header-title { font-size: 1.1rem; font-weight: 800; }
.modal__header-sub { font-size: .78rem; color: rgba(255,255,255,.7); }
.modal__close {
background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.2);
color: var(--white); width: 30px; height: 30px; border-radius: 50%;
font-size: 1.1rem; cursor: pointer; line-height: 1;
display: flex; align-items: center; justify-content: center;
transition: background var(--transition); flex-shrink: 0;
}
.modal__close:hover { background: rgba(255,255,255,.25); }
.modal__body { padding: 20px 24px 24px; }
.modal__cards { display: grid; gap: 10px; margin-bottom: 14px; }
.modal__cards--2 { grid-template-columns: 1fr 1fr; }
.modal__card {
display: flex; align-items: center; gap: 12px;
background: var(--light); border: 1px solid var(--border);
border-radius: var(--radius); padding: 13px 16px;
text-decoration: none; color: var(--dark);
transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
}
.modal__card:hover { border-color: var(--blue); box-shadow: 0 2px 12px rgba(0,143,204,.12); background: #f0f9ff; text-decoration: none; }
.modal__card-icon {
width: 36px; height: 36px; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem; flex-shrink: 0;
}
.modal__card-icon--blue { background: var(--blue-light); }
.modal__card-icon--yellow { background: #fffbe6; }
.modal__card-icon--green { background: #e8f5e9; }
.modal__card-icon--purple { background: #f3e5f5; }
.modal__card-text { flex: 1; }
.modal__card-title { font-size: .85rem; font-weight: 700; color: var(--dark); line-height: 1.3; }
.modal__card-desc { font-size: .75rem; color: var(--muted); line-height: 1.3; margin-top: 2px; }
.modal__card-arrow { color: var(--blue); font-size: .9rem; opacity: 0; transition: opacity var(--transition), transform var(--transition); }
.modal__card:hover .modal__card-arrow { opacity: 1; transform: translateX(3px); }
.modal__section-label {
font-size: .68rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .1em; color: var(--muted); margin: 16px 0 8px;
}
.modal__store-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.modal__store-btn {
display: inline-flex; align-items: center; gap: 8px;
background: var(--dark); color: var(--white);
border-radius: var(--radius); padding: 8px 14px;
font-size: .8rem; font-weight: 600;
transition: background var(--transition);
}
.modal__store-btn:hover { background: #444; text-decoration: none; color: var(--white); }
.modal__store-btn img[src*="logo-app-store"], .sol-card__store img[src*="logo-app-store"] { filter: invert(1); }
/* Split store button */
.store-split {
display: inline-flex; border-radius: var(--radius); overflow: hidden;
border: 1px solid #555;
}
.store-split__half {
display: inline-flex; align-items: center; gap: 7px;
background: var(--dark); color: var(--white);
padding: 8px 14px; font-size: .8rem; font-weight: 600;
text-decoration: none; transition: background var(--transition);
white-space: nowrap;
}
.store-split__half:hover { background: #444; color: var(--white); text-decoration: none; }
.store-split__half + .store-split__half { border-left: 1px solid #555; }
.store-split__half img[src*="logo-app-store"] { filter: invert(1); }
/* Same split style for sol-cards (dark background) */
.sol-card__store-split {
display: inline-flex; border-radius: 100px; overflow: hidden;
border: 1.5px solid rgba(255,255,255,.22);
}
.sol-card__store-split__half {
display: inline-flex; align-items: center; gap: 5px;
background: transparent; color: rgba(255,255,255,.7);
padding: 7px 12px; font-size: .78rem; font-weight: 600;
text-decoration: none; transition: background .15s, color .15s;
white-space: nowrap;
}
.sol-card__store-split__half:hover { background: rgba(255,255,255,.08); color: #fff; text-decoration: none; }
.sol-card__store-split__half + .sol-card__store-split__half { border-left: 1px solid rgba(255,255,255,.22); }
.sol-card__store-split__half img[src*="logo-app-store"] { filter: invert(1); opacity: .8; }
/* ── Modal image slider ── */
.modal__img-slider { position: relative; background: #111; overflow: hidden; height: 280px; }
.modal__img-track { display: flex; height: 100%; transition: transform .4s cubic-bezier(.4,0,.2,1); }
.modal__img-track img { width: 100%; flex-shrink: 0; object-fit: cover; height: 280px; display: block; }
.modal__img-btn {
position: absolute; top: 50%; transform: translateY(-50%);
background: rgba(0,0,0,.45); border: none; color: #fff;
width: 32px; height: 32px; border-radius: 50%; cursor: pointer;
font-size: 1rem; display: flex; align-items: center; justify-content: center;
z-index: 2; transition: background .15s;
}
.modal__img-btn:hover { background: rgba(0,143,204,.7); }
.modal__img-btn--prev { left: 8px; }
.modal__img-btn--next { right: 8px; }
.modal__img-dots {
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
display: flex; gap: 5px;
}
.modal__img-dot {
width: 6px; height: 6px; border-radius: 50%; border: none; padding: 0;
background: rgba(255,255,255,.45); cursor: pointer; transition: background .15s;
}
.modal__img-dot.active { background: #fff; }
/* ════════════ DOCS CARD GRID ════════════ */
.docs__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 48px;
}
.docs__card {
background: var(--white);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.docs__card-head {
display: flex; align-items: center; gap: 12px;
padding: 18px 20px 16px;
border-bottom: 1px solid var(--border);
}
.docs__card-icon {
width: 38px; height: 38px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; flex-shrink: 0;
}
.docs__card-head h3 { font-size: .95rem; color: var(--dark); margin: 0; }
.docs__card-links { padding: 10px 12px 12px; display: flex; flex-direction: column; gap: 2px; }
.docs__link {
display: flex; align-items: center; justify-content: space-between;
padding: 9px 10px;
border-radius: 6px;
font-size: .84rem; font-weight: 600; color: var(--dark-mid);
transition: background .15s, color .15s;
text-decoration: none;
}
.docs__link:hover { background: var(--light); color: var(--blue); text-decoration: none; }
.docs__link-arrow { font-size: .75rem; opacity: 0; transition: opacity .15s, transform .15s; flex-shrink: 0; }
.docs__link:hover .docs__link-arrow { opacity: 1; transform: translateX(3px); }
@media (max-width: 900px) { .docs__grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .docs__grid { grid-template-columns: 1fr; } }
/* ════════════ TECH STACK ════════════ */
.tech__grid {
display: flex; flex-wrap: wrap; gap: 16px;
justify-content: center; margin-top: 48px;
}
.tech-item {
display: flex; flex-direction: column; align-items: center; gap: 8px;
background: var(--white); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: 20px 20px 16px;
min-width: 120px; max-width: 148px;
box-shadow: var(--shadow-sm);
}
.tech-item img { width: 40px; height: 40px; object-fit: contain; }
.tech-item span { font-size: .78rem; font-weight: 700; color: var(--dark); text-align: center; }
.tech-item small { font-size: .7rem; color: var(--muted); text-align: center; line-height: 1.4; }
/* Tech items on dark background */
.getting-started .tech-item {
background: rgba(255,255,255,.07); border-color: rgba(255,255,255,.12);
box-shadow: none;
}
.getting-started .tech-item span { color: rgba(255,255,255,.9); }
.getting-started .tech-item small { color: rgba(255,255,255,.5); }
/* Java and Jetty use dark #333 - invert to white on dark bg */
.getting-started .tech-item--invert img { filter: brightness(0) invert(1) opacity(.85); }
/* ════════════ DOCS GRAPH ════════════ */
/* ════════════ WHAT'S NEW ════════════ */
.accordion { margin-top: 48px; display: flex; flex-direction: column; gap: 0; border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-md); border: 1px solid var(--border); }
.accordion-item { border-bottom: 1px solid var(--border); }
.accordion-item:last-child { border-bottom: none; }
.accordion-header {
display: flex; align-items: center; justify-content: space-between;
padding: 20px 28px; background: var(--white);
cursor: pointer; user-select: none;
transition: background var(--transition);
border-left: 4px solid transparent;
}
.accordion-header:hover { background: var(--light); }
.accordion-header.open {
background: linear-gradient(135deg, #1c2e3e 0%, #0d1f2d 100%);
border-left-color: var(--blue);
}
.accordion-title {
display: flex; align-items: center; gap: 14px;
font-size: 1rem; font-weight: 700; color: var(--dark);
}
.accordion-header.open .accordion-title { color: var(--white); }
.accordion-tag {
font-size: .68rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
padding: 3px 10px; border-radius: 100px;
}
.tag-web { background: rgba(0,143,204,.12); color: var(--blue); border: 1px solid rgba(0,143,204,.25); }
.tag-android{ background: rgba(46,125,50,.1); color: #2e7d32; border: 1px solid rgba(46,125,50,.2); }
.tag-ios { background: rgba(106,27,154,.08); color: #6a1b9a; border: 1px solid rgba(106,27,154,.18); }
.accordion-header.open .tag-web { background: rgba(0,143,204,.2); color: #82d2ff; border-color: rgba(0,143,204,.4); }
.accordion-header.open .tag-android{ background: rgba(102,187,106,.15); color: #81c784; border-color: rgba(102,187,106,.3); }
.accordion-header.open .tag-ios { background: rgba(206,147,216,.12); color: #ce93d8; border-color: rgba(206,147,216,.3); }
.accordion-arrow {
font-size: 1.1rem; color: var(--muted); transition: transform var(--transition);
}
.accordion-header.open .accordion-arrow { transform: rotate(180deg); color: rgba(255,255,255,.6); }
.accordion-body {
display: none; background: var(--white);
border-top: 1px solid var(--border);
}
.accordion-body.open { display: block; }
/* Styles applied to the injected source HTML content */
.whatsnew-content { padding: 8px 28px 24px; }
.whatsnew-content h5, .whatsnew-content .articleSubheadline {
font-size: .78rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .08em; color: var(--blue);
margin: 20px 0 8px; padding: 0;
}
.whatsnew-content ul, .whatsnew-content .bulletList {
list-style: none; display: flex; flex-direction: column; gap: 6px;
padding: 0; margin: 0;
}
.whatsnew-content li {
font-size: .875rem; color: var(--muted); line-height: 1.6;
padding-left: 14px; position: relative;
}
.whatsnew-content li::before { content: '–'; position: absolute; left: 0; color: var(--yellow); font-weight: 700; }
.whatsnew-content a { color: var(--blue); }
.whatsnew-content h4, .whatsnew-content .articleHeadline { display: none; }
.whatsnew-content tt, .whatsnew-content code {
font-family: 'JetBrains Mono', monospace; font-size: .82em;
background: #f0f4f8; border-radius: 3px; padding: 1px 4px; color: #c7254e;
}
.whatsnew-content p { display: inline; }
.accordion-loading {
padding: 24px; text-align: center; color: var(--muted); font-size: .85rem;
}
/* Floating close button for open accordions */
.accordion-close-fab { display: none; }
/* Sticky "Close all" bar - fixed, only visible while an accordion in #whatsnew is open */
.whatsnew__header {
display: flex; align-items: flex-end; justify-content: space-between;
flex-wrap: wrap; gap: 12px; margin-bottom: 0;
}
.whatsnew__sticky-bar {
display: none;
position: sticky; top: 68px; z-index: 100;
justify-content: flex-end;
pointer-events: none;
margin-bottom: -48px; padding-top: 8px;
}
.whatsnew__sticky-bar.visible { display: flex; pointer-events: auto; }
.whatsnew__close-btn {
display: flex; align-items: center; gap: 6px;
background: var(--blue); color: var(--white);
border: none; border-radius: 100px; padding: 9px 20px;
font-size: .82rem; font-weight: 700;
cursor: pointer; box-shadow: 0 2px 16px rgba(0,143,204,.35);
transition: background .15s; white-space: nowrap;
}
.whatsnew__close-btn:hover { background: var(--blue-dark); }
/* ════════════ GETTING STARTED ════════════ */
.getting-started { background: #192533; color: var(--white); }
.getting-started .eyebrow { color: var(--yellow); }
.getting-started h2 { color: var(--white); }
.getting-started .lead { color: rgba(255,255,255,.7); }
.steps { display: grid; grid-template-columns: repeat(3,1fr); gap: 24px; margin-top: 48px; }
.step {
background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1);
border-radius: var(--radius-lg); padding: 28px;
}
.step__num {
width: 34px; height: 34px; border-radius: 50%;
background: var(--blue); color: var(--white);
display: flex; align-items: center; justify-content: center;
font-size: .85rem; font-weight: 700; margin-bottom: 16px;
}
.step h3 { color: var(--white); font-size: .95rem; margin-bottom: 8px; }
.step p { color: rgba(255,255,255,.6); font-size: .875rem; }
.code-block {
margin-top: 40px;
background: rgba(0,0,0,.3); border: 1px solid rgba(255,255,255,.1);
border-radius: var(--radius-lg); overflow: hidden;
}
.code-block__bar {
display: flex; align-items: center; gap: 6px;
padding: 12px 18px; background: rgba(255,255,255,.04);
border-bottom: 1px solid rgba(255,255,255,.08);
}
.code-block__bar span { width: 10px; height: 10px; border-radius: 50%; }
.code-block__bar span:nth-child(1) { background: #ff5f57; }
.code-block__bar span:nth-child(2) { background: #ffbd2e; }
.code-block__bar span:nth-child(3) { background: #28c840; }
.code-block pre {
padding: 22px 24px;
font-family: 'JetBrains Mono', monospace; font-size: .875rem;
line-height: 1.8; color: rgba(255,255,255,.85); overflow-x: auto;
}
.cc { color: rgba(255,255,255,.35); }
.ck { color: #4caf50; }
.cv { color: var(--yellow); }
/* ════════════ REVIEWS ════════════ */
.reviews {
background: #e8ecf0;
position: relative; overflow: hidden;
}
.reviews::before {
content: ''; position: absolute; inset: 0; pointer-events: none;
background:
radial-gradient(ellipse 700px 400px at 0% 60%, rgba(0,143,204,.07) 0%, transparent 70%),
radial-gradient(ellipse 500px 400px at 100% 20%, rgba(239,170,0,.05) 0%, transparent 70%);
}
.reviews .eyebrow { color: var(--blue); }
.reviews h2 { color: var(--text); }
.reviews .lead { color: var(--muted); }
.reviews__ticker-wrap {
position: relative; margin-top: 44px;
overflow-x: auto; overflow-y: visible;
padding-top: 8px;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
cursor: grab;
}
.reviews__ticker-wrap:active { cursor: grabbing; }
/* hide scrollbar */
.reviews__ticker-wrap::-webkit-scrollbar { display: none; }
.reviews__ticker-wrap { -ms-overflow-style: none; scrollbar-width: none; }
.reviews__ticker {
display: flex; gap: 24px;
width: max-content;
}
.rev-card {
flex-shrink: 0; width: 340px;
background: var(--white);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 26px 26px 22px;
position: relative;
display: flex; flex-direction: column;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: border-color .2s, transform .2s, box-shadow .2s;
}
.rev-card:hover {
border-color: rgba(0,143,204,.4);
box-shadow: var(--shadow-md);
transform: translateY(-3px);
}
.rev-card__quote-mark {
font-size: 4rem; line-height: 1; color: var(--blue);
opacity: .12; position: absolute; top: 14px; right: 16px;
font-family: Georgia, serif; user-select: none;
}
.rev-card__body-wrap {
height: 120px; overflow: hidden; position: relative; margin-bottom: 18px;
}
.rev-card__body-wrap.overflows::after {
content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 56px;
background: linear-gradient(to bottom, transparent, var(--white));
pointer-events: none;
}
.rev-card__body-wrap.overflows .rev-card__chevron {
display: flex;
}
.rev-card__chevron {
display: none;
position: absolute; bottom: 4px; left: 0; right: 0;
justify-content: center;
color: var(--blue); font-size: 1.5rem; line-height: 1;
pointer-events: none;
z-index: 4;
opacity: .6;
transition: opacity .2s, bottom .2s;
}
.rev-card:hover .rev-card__chevron { opacity: 1; bottom: 7px; }
.rev-card__body {
font-size: .875rem; color: var(--text);
line-height: 1.7; padding-right: 20px;
}
.rev-card__footer {
display: flex; align-items: center; gap: 12px;
border-top: 1px solid var(--border);
padding-top: 14px; margin-top: auto;
}
/* Ticker nav arrows */
.reviews__nav {
display: flex; align-items: center; justify-content: center;
gap: 28px; margin-top: 40px;
}
.reviews__nav-btn {
background: none; border: none; cursor: pointer;
color: rgba(0,0,0,.3);
font-size: 1.6rem; font-weight: 400; line-height: 1;
padding: 4px 10px;
letter-spacing: -1px;
transition: color .2s;
}
.reviews__nav-btn:hover { color: var(--blue); }
.rev-card__avatar {
width: 48px; height: 48px; border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(0,143,204,.3);
flex-shrink: 0;
}
.rev-card__avatar--top { object-position: center top; }
.rev-card__avatar--face { object-position: center 20%; }
.rev-card__avatar-placeholder {
width: 48px; height: 48px; border-radius: 50%;
background: linear-gradient(135deg, var(--blue-dark), var(--blue));
display: flex; align-items: center; justify-content: center;
font-size: 1rem; color: var(--white); font-weight: 700;
flex-shrink: 0; border: 2px solid rgba(0,143,204,.3); line-height: 1;
}
.rev-card__name { font-size: .85rem; font-weight: 700; color: var(--dark); line-height: 1.3; }
.rev-card__role { font-size: .73rem; color: var(--muted); margin-top: 2px; }
.rev-card__org { font-size: .68rem; color: var(--blue); font-weight: 700; text-transform: uppercase; letter-spacing: .07em; margin-top: 3px; }
/* Review modal */
.rev-modal-overlay {
display: none; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.5); backdrop-filter: blur(4px);
align-items: center; justify-content: center; padding: 24px;
}
.rev-modal-overlay.open { display: flex; }
.rev-modal {
background: var(--white); border: 1px solid var(--border);
border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
width: 100%; max-width: 600px;
position: relative; animation: modalIn .2s ease;
display: flex; overflow: hidden; max-height: 90vh;
}
.rev-modal__photo {
width: 180px; flex-shrink: 0;
background: var(--surface);
overflow: hidden;
}
.rev-modal__photo img {
width: 100%; height: 100%; object-fit: cover;
}
.rev-modal__photo-placeholder {
width: 100%; height: 100%;
background: linear-gradient(135deg, var(--blue-dark), var(--blue));
display: flex; align-items: center; justify-content: center;
font-size: 3rem; color: var(--white); font-weight: 700;
}
.rev-modal__content {
flex: 1; padding: 44px 28px 24px; min-width: 0;
overflow-y: auto; position: relative;
}
.rev-modal__quote-mark {
font-size: 5rem; line-height: 1; color: var(--blue); opacity: .08;
position: absolute; top: 8px; right: 16px;
font-family: Georgia, serif; user-select: none;
}
.rev-modal__body {
font-size: .9rem; color: var(--text);
line-height: 1.75; margin-bottom: 20px;
}
.rev-modal__footer {
display: flex; align-items: center; gap: 14px;
border-top: 1px solid var(--border); padding-top: 16px;
}
.rev-modal__close {
position: absolute; top: 10px; right: 10px;
background: rgba(0,0,0,.06); border: 1px solid rgba(0,0,0,.1);
color: var(--muted); width: 28px; height: 28px; border-radius: 50%;
font-size: 1rem; cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background .15s; z-index: 2; flex-shrink: 0;
}
.rev-modal__close:hover { background: rgba(0,0,0,.12); color: var(--text); }
/* ════════════ FOOTER ════════════ */
.footer { background: #0a1929; color: rgba(255,255,255,.6); padding: 56px 0 28px; }
.footer__grid {
display: grid; grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 48px; margin-bottom: 48px;
}
.footer__brand img { height: 36px; margin-bottom: 16px; }
.footer__brand p { font-size: .875rem; line-height: 1.7; max-width: 260px; color: rgba(255,255,255,.82); }
.footer__social { display: flex; gap: 12px; margin-top: 20px; }
.footer__social a {
width: 36px; height: 36px; border-radius: 50%;
background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12);
display: flex; align-items: center; justify-content: center;
transition: background var(--transition);
}
.footer__social a:hover { background: rgba(255,255,255,.18); text-decoration: none; }
.footer__social img { height: 18px; width: auto; filter: invert(1) brightness(2); }
.footer__col h4 {
font-size: .75rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .09em; color: rgba(255,255,255,.35); margin-bottom: 14px;
}
.footer__col ul { list-style: none; display: flex; flex-direction: column; gap: 9px; }
.footer__col ul li a {
font-size: .875rem; color: rgba(255,255,255,.6);
transition: color var(--transition);
}
.footer__col ul li a:hover { color: var(--white); text-decoration: none; }
.footer__bottom {
border-top: 1px solid rgba(255,255,255,.1);
padding-top: 22px;
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 12px;
}
.footer__bottom p { font-size: .8rem; color: rgba(255,255,255,.4); }
.footer__bottom-links { display: flex; gap: 20px; flex-wrap: wrap; }
.footer__bottom-links a { font-size: .8rem; color: rgba(255,255,255,.4); transition: color var(--transition); }
.footer__bottom-links a:hover { color: var(--white); text-decoration: none; }
/* ════════════ RESPONSIVE ════════════ */
@media (max-width: 1024px) {
.footer__grid { grid-template-columns: 1fr 1fr; gap: 32px; }
}
@media (max-width: 768px) {
.section { padding: 56px 0; }
.steps { grid-template-columns: 1fr; }
.solutions__track .sol-card { flex: 0 0 calc(100% - 0px); }
.footer__grid { grid-template-columns: 1fr; }
.nav__links { display: none; }
.hero__stats { gap: 24px; }
}
</style>
</head>
<body>
<!-- ═══════════════════════════════════ NAV ═══════════════════════════════════ -->
<nav class="nav">
<div class="container nav__inner">
<a href="#" style="display:flex;align-items:center;gap:10px;text-decoration:none;">
<img src="source/logo-azimuth-no-bg.png" alt="Azimuth" style="height:48px;width:auto;filter:brightness(0) invert(1);">
</a>
<div class="nav__links">
<a href="#solutions">Solutions</a>
<a href="#reviews">Reviews</a>
<a href="#getting-started">Quick Start</a>
<a href="#docs">Documentation</a>
<a href="#tech">Technology</a>
<a href="#whatsnew">What's New</a>
<a class="btn btn-ghost nav__cta" href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>GitHub</a>
</div>
</div>
</nav>
<!-- ═══════════════════════════════════ HERO ═══════════════════════════════════ -->
<section class="hero">
<div class="hero__bg"><img src="source/regatta-bg.webp" alt=""></div>
<div class="container">
<div class="hero__content">
<a class="hero__badge" href="https://github.com/eclipse-sailing-analytics/sailing-analytics?tab=Apache-2.0-1-ov-file#" target="_blank" rel="noopener" style="text-decoration:none;">
<span class="hero__badge-dot"></span>
Open Source &mdash; Apache 2.0
</a>
<h1>Open-source analytics for <span>competitive sailing</span></h1>
<p class="hero__sub">
Eclipse Azimuth Sailing Analytics is a community-driven platform for live GPS tracking,
real-time race analysis, and fan engagement - from club regattas to the Olympic Games.
Free to use, free to contribute.
</p>
<div class="hero__actions">
<a class="btn btn-primary" href="#getting-started">Get Started</a>
<a class="btn btn-outline-white" href="https://www.sapsailing.com/gwt/Home.html" target="_blank" rel="noopener">Open Application</a>
<a class="btn btn-outline-white" href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">View on GitHub</a>
</div>
<div class="hero__stats">
<div>
<div class="hero__stat-value">2×</div>
<div class="hero__stat-label">Olympic Games</div>
</div>
<div>
<div class="hero__stat-value">~1M</div>
<div class="hero__stat-label">Olympic visitors</div>
</div>
<div>
<div class="hero__stat-value">15+</div>
<div class="hero__stat-label">GPS connectors</div>
</div>
<div>
<div class="hero__stat-value">225+</div>
<div class="hero__stat-label">OSGi modules</div>
</div>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════ SOLUTIONS ══════════════════════════════ -->
<section class="section" id="solutions">
<div class="container">
<p class="eyebrow eyebrow--yellow">Solutions</p>
<h2>A full suite of sailing tools</h2>
<p class="lead" style="max-width:600px;margin-top:10px;">
From the main cloud platform to companion mobile apps, every component is open-source
and designed to work together.
</p>
<div class="solutions__carousel-wrap">
<div class="solutions__track-outer">
<div class="solutions__track" id="sol-track">
<!-- Sailing Analytics -->
<div class="sol-card">
<div class="sol-card__img">
<img src="source/sailing-analytics-Photoroom.png" alt="Eclipse Azimuth Sailing Analytics">
</div>
<div class="sol-card__body">
<span class="sol-card__tag sol-card__tag--web">Web Platform</span>
<h3>Eclipse Azimuth Sailing Analytics</h3>
<p>Real-time GPS analytics for rankings, speeds, maneuvers, and bearings - from club regattas to the Olympic Games. Processes live telemetry in the cloud and visualizes it in multiple frontends.</p>
<div class="sol-card__links">
<button class="btn btn-primary" onclick="openModal('modal-sa')">Learn More</button>
<a class="sol-card__store" href="https://www.sapsailing.com/gwt/Home.html" target="_blank" rel="noopener"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>Open App</a>
</div>
</div>
</div>
<!-- Race Manager -->
<div class="sol-card">
<div class="sol-card__img">
<img src="source/sailing-race-manager-1.png" alt="Sailing Race Manager">
</div>
<div class="sol-card__body">
<span class="sol-card__tag sol-card__tag--android">Android App</span>
<h3>Sailing Race Manager</h3>
<p>Android app for race committees. Manage start sequences, record finishes, handle flags, and sync results directly to the Sailing Analytics cloud from a tablet on the committee boat.</p>
<div class="sol-card__links">
<button class="btn btn-primary" onclick="openModal('modal-rm')">Learn More</button>
<a class="sol-card__store" href="https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app" target="_blank" rel="noopener"><img src="source/logo-google-play.png" width="13" height="13" alt="" style="display:inline-block;vertical-align:middle;">Google Play</a>
</div>
</div>
</div>
<!-- Sail Insight -->
<div class="sol-card">
<div class="sol-card__img">
<img src="source/sail-insight-1-Photoroom.png" alt="Sail Insight">
</div>
<div class="sol-card__body">
<span class="sol-card__tag sol-card__tag--ios">iOS &amp; Android App</span>
<h3>Sail Insight</h3>
<p>Sailors can join, set up, and manage GPS tracking for regattas of various formats. One-design and handicap regattas supported. Connects to the Eclipse Azimuth Sailing Analytics cloud for deep analysis.</p>
<div class="sol-card__links">
<button class="btn btn-primary" onclick="openModal('modal-si')">Learn More</button>
<div class="sol-card__store-split">
<a class="sol-card__store-split__half" href="https://apps.apple.com/us/app/sail-insight-powered-by-sap/id1495355086" target="_blank" rel="noopener"><img src="source/logo-app-store.png" width="12" height="12" alt="">App Store</a>
<a class="sol-card__store-split__half" href="https://play.google.com/store/apps/details?id=org.sailyachtresearch.sailinsight" target="_blank" rel="noopener"><img src="source/logo-google-play.png" width="12" height="12" alt="">Google Play</a>
</div>
</div>
</div>
</div>
<!-- Buoy Pinger -->
<div class="sol-card">
<div class="sol-card__img">
<img src="source/sailing-buoy-pinger-1-Photoroom.png" alt="Sailing Buoy Pinger">
</div>
<div class="sol-card__body">
<span class="sol-card__tag sol-card__tag--android">Android App</span>
<h3>Sailing Buoy Pinger</h3>
<p>Simplify race mark positioning and communication with mark layers. Connects to Eclipse Azimuth Sailing Analytics so regatta managers can track mark positions and enable Sail Insight GPS tracking.</p>
<div class="sol-card__links">
<button class="btn btn-primary" onclick="openModal('modal-bp')">Learn More</button>
<a class="sol-card__store" href="https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app" target="_blank" rel="noopener"><img src="source/logo-google-play.png" width="13" height="13" alt="" style="display:inline-block;vertical-align:middle;">Google Play</a>
</div>
</div>
</div>
<!-- Strategy Simulator -->
<div class="sol-card">
<div class="sol-card__img">
<img src="source/strategy-simulator-Photoroom.png" alt="Strategy Simulator">
</div>
<div class="sol-card__body">
<span class="sol-card__tag sol-card__tag--web">Web Platform</span>
<h3>Strategy Simulator</h3>
<p>Determine the best sailing strategy for various wind conditions and currents. Wind and current can be changed in real time, so the robustness of a strategy can be evaluated under uncertainty.</p>
<div class="sol-card__links">
<button class="btn btn-primary" onclick="openModal('modal-ss')">Learn More</button>
<a class="sol-card__store" href="https://www.sapsailing.com/gwt/Simulator.html" target="_blank" rel="noopener"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>Open App</a>
</div>
</div>
</div>
</div>
</div>
<div class="solutions__nav">
<button class="solutions__arrow" id="sol-prev" onclick="solNav(-1)" aria-label="Previous">&#8249;</button>
<div class="solutions__dots" id="sol-dots"></div>
<button class="solutions__arrow" id="sol-next" onclick="solNav(1)" aria-label="Next">&#8250;</button>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════ REVIEWS ══════════════════════════════ -->
<section class="section reviews" id="reviews">
<div class="container">
<p class="eyebrow">User Feedback 2026</p>
<h2>Trusted by the sailing world</h2>
<p class="lead" style="max-width:560px;margin-top:10px;">
From club regattas to international championships - what organizers, coaches, and sailors say.
</p>
</div>
<div class="reviews__ticker-wrap">
<div class="reviews__ticker" id="rev-ticker">
<div class="rev-card" data-modal="rmod-frank" onclick="openRevModal('rmod-frank')">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">With SAP's return to processing tracked data at Travemünder Woche 2026, 42 races across 9 boat classes could be processed in a graphically appealing way and presented attractively for the interested public. An excellent instrument for our participants to analyse their own performance - a genuine added value!</p><span class="rev-card__chevron">&#8964;</span></div>
<div class="rev-card__footer">
<img class="rev-card__avatar rev-card__avatar--top" src="source/frank_scharffe.png" alt="Frank Schärffe">
<div><div class="rev-card__name">Frank Schärffe</div><div class="rev-card__role">Managing Director</div><div class="rev-card__org">Travemünder Woche gGmbH</div></div>
</div>
</div>
<div class="rev-card" data-modal="rmod-sven" onclick="openRevModal('rmod-sven')">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">SAP Sailing Analytics is an extremely helpful tool for Kieler Woche in many respects. Not only the analysis itself, but also the presentation and the depth of information behind it help us convey sailing in near real-time in a way that is broadly understandable and exciting.</p><span class="rev-card__chevron">&#8964;</span></div>
<div class="rev-card__footer">
<img class="rev-card__avatar rev-card__avatar--face" src="source/Sven Christensen-0fc6b9f9.png" alt="Sven Christensen">
<div><div class="rev-card__name">Sven Christensen</div><div class="rev-card__role">CEO, Point of Sailing</div><div class="rev-card__org">Kieler Woche</div></div>
</div>
</div>
<div class="rev-card" data-modal="rmod-stanislaw" onclick="openRevModal('rmod-stanislaw')">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">As the organizers of the Polish Sailing League, integrating SAP Sailing Analytics has been a complete game-changer for our fast-paced regattas. The system's live tracking effortlessly translates complex tactical decisions into an engaging, easily digestible experience for our fans and commentators.</p><span class="rev-card__chevron">&#8964;</span></div>
<div class="rev-card__footer">
<img class="rev-card__avatar rev-card__avatar--top" src="source/stanislav_konarzewski.jpg" alt="Stanisław Konarzewski">
<div><div class="rev-card__name">Stanisław Konarzewski</div><div class="rev-card__role">Marketing &amp; Communication Manager</div><div class="rev-card__org">Polish Sailing League</div></div>
</div>
</div>
<div class="rev-card" data-modal="rmod-jack" onclick="openRevModal('rmod-jack')">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">SAP Sailing Analytics strikes a unique balance of being accessible to any average club racer on a budget, and powerful enough to satisfy the most complex regatta or performance analytics far better than its competitors.</p><span class="rev-card__chevron">&#8964;</span></div>
<div class="rev-card__footer">
<div class="rev-card__avatar-placeholder">J</div>
<div><div class="rev-card__name">Jack V.</div><div class="rev-card__role">Pacific Northwest Sailor</div><div class="rev-card__org">Sidney North Saanich YC</div></div>
</div>
</div>
<div class="rev-card">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">The live race tracker kept our entire membership glued to the screen during the championship weekend. Incredibly easy to configure from the committee boat.</p></div>
<div class="rev-card__footer">
<div class="rev-card__avatar-placeholder">A</div>
<div><div class="rev-card__name">Anna Bergström</div><div class="rev-card__role">Club Commodore</div><div class="rev-card__org">Gothenburg Sailing Society</div></div>
</div>
</div>
<div class="rev-card">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">Finally a tool that lets our coaches review every tack and gybe after the race. The data mining view is exactly what we needed for athlete development.</p></div>
<div class="rev-card__footer">
<div class="rev-card__avatar-placeholder">T</div>
<div><div class="rev-card__name">Thomas Müller</div><div class="rev-card__role">Head Coach</div><div class="rev-card__org">Bodensee Sailing Academy</div></div>
</div>
</div>
<div class="rev-card">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">We used it for a 200-boat offshore race and it handled everything flawlessly. The tracking coverage and update frequency impressed even our most skeptical committee members.</p></div>
<div class="rev-card__footer">
<div class="rev-card__avatar-placeholder">R</div>
<div><div class="rev-card__name">Rachel O'Brien</div><div class="rev-card__role">Race Director</div><div class="rev-card__org">Cork Harbour Regatta</div></div>
</div>
</div>
<div class="rev-card">
<div class="rev-card__quote-mark">&ldquo;</div>
<div class="rev-card__body-wrap"><p class="rev-card__body">The broadcast overlay integration was seamless. Our commentators had everything they needed on screen and the audience engagement went through the roof.</p></div>
<div class="rev-card__footer">
<div class="rev-card__avatar-placeholder">L</div>
<div><div class="rev-card__name">Lucas Fontaine</div><div class="rev-card__role">Broadcast Producer</div><div class="rev-card__org">Sailing France TV</div></div>
</div>
</div>
</div>
</div>
<div class="container">
<div class="reviews__nav">
<button class="reviews__nav-btn" onclick="revTickerStep(-1)">&#8249;</button>
<button class="reviews__nav-btn" onclick="revTickerStep(1)">&#8250;</button>
</div>
</div>
</section>
<!-- ═══════════════════════════════════ GETTING STARTED ═══════════════════════ -->
<section class="section getting-started" id="getting-started">
<div class="container">
<p class="eyebrow">Quick Start</p>
<h2>Up and running in minutes</h2>
<p class="lead" style="max-width:540px;margin-top:10px;">
The fastest way to explore Eclipse Azimuth Sailing Analytics locally is via Docker Compose —
no build required, just pull and run.
</p>
<div class="steps">
<div class="step">
<div class="step__num">1</div>
<h3>Prerequisites</h3>
<p>Install <strong style="color:rgba(255,255,255,.85)">Docker</strong> and
<strong style="color:rgba(255,255,255,.85)">Docker Compose</strong>.
Works on Linux, macOS, and Windows with bash.</p>
</div>
<div class="step">
<div class="step__num">2</div>
<h3>Fetch &amp; Launch</h3>
<p>Download the Compose file and start MongoDB, RabbitMQ, and Sailing Analytics
with a single command.</p>
</div>
<div class="step">
<div class="step__num">3</div>
<h3>Open the UI</h3>
<p>Navigate to <strong style="color:rgba(255,255,255,.85)">http://127.0.0.1:8888</strong>
and log in to the Admin Console with
<code style="color:var(--yellow)">admin / admin</code>.</p>
</div>
</div>
<div class="code-block">
<div class="code-block__bar"><span></span><span></span><span></span></div>
<pre><span class="cc"># Download the Docker Compose configuration</span>
<span class="ck">wget</span> <span class="cv">"https://github.com/eclipse-sailing-analytics/sailing-analytics/raw/main/docker/docker-compose.yml"</span>
<span class="cc"># Start MongoDB, RabbitMQ, and Sailing Analytics</span>
<span class="ck">docker-compose</span> up
<span class="cc"># Open the Admin Console at http://127.0.0.1:8888/gwt/AdminConsole.html</span>
<span class="cc"># Default credentials: admin / admin</span></pre>
</div>
<div style="display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-top:28px;">
<a class="btn btn-yellow" href="https://wiki.sapsailing.com/wiki/howto/onboarding.md" target="_blank" rel="noopener">Full Onboarding Guide</a>
<a class="btn btn-outline-white" href="https://github.com/eclipse-sailing-analytics/sailing-analytics/releases" target="_blank" rel="noopener">Latest Release</a>
<a class="btn btn-outline-white" href="https://github.com/eclipse-sailing-analytics/sailing-analytics/pkgs/container/sailing-analytics" target="_blank" rel="noopener">Docker Image</a>
<span style="margin-left:auto;display:inline-flex;align-items:center;gap:8px;padding:9px 16px;background:rgba(239,170,0,.12);border:1px solid rgba(239,170,0,.35);border-radius:var(--radius);border-left:3px solid var(--yellow);">
<span style="font-size:.9rem;font-weight:900;color:var(--yellow);line-height:1;">!</span>
<span style="font-size:.82rem;color:rgba(255,255,255,.8);">Found a bug or have a feature request? Use the <a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/issues/" target="_blank" rel="noopener" style="color:var(--yellow);font-weight:700;">issue tracker</a> on GitHub.</span>
</span>
</div>
</div>
</section>
<!-- ═══════════════════════════════════ DOCUMENTATION ════════════════════════ -->
<section class="section section--alt" id="docs">
<div class="container">
<p class="eyebrow">Documentation</p>
<h2>Everything you need to go further</h2>
<p class="lead" style="max-width:560px;margin-top:10px;">
Browse by category and click any topic to open the <a href="https://wiki.sapsailing.com/Home" target="_blank" rel="noopener">wiki</a>.
</p>
<div class="docs__grid" id="docs-grid"></div>
</div>
</section>
<!-- ═══════════════════════════════════ TECHNOLOGY ════════════════════════════ -->
<section class="section getting-started" id="tech">
<div class="container">
<p class="eyebrow">Technology</p>
<h2>Built on proven open-source foundations</h2>
<p class="lead" style="max-width:560px;margin-top:10px;">
A polyglot stack - Java backend, GWT frontend, mobile apps - all tied together
by battle-tested middleware and cloud infrastructure.
</p>
<div class="tech__grid">
<div class="tech-item tech-item--invert">
<img src="https://cdn.simpleicons.org/openjdk/333333" alt="Java">
<span>Java 8+</span>
<small>Core server runtime &amp; all business logic</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/eclipseide/2C2255" alt="OSGi / Eclipse">
<span>OSGi / Equinox</span>
<small>Modular plugin architecture - 225+ bundles</small>
</div>
<div class="tech-item">
<img src="source/logo-gwt.svg" alt="GWT">
<span>GWT</span>
<small>Java-to-JavaScript web frontend compiler</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/mongodb/47A248" alt="MongoDB">
<span>MongoDB</span>
<small>Primary store for race &amp; GPS data</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/rabbitmq/FF6600" alt="RabbitMQ">
<span>RabbitMQ</span>
<small>Message broker used for service replication</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/docker/2496ED" alt="Docker">
<span>Docker</span>
<small>Containerized deployment &amp; local dev</small>
</div>
<div class="tech-item">
<img src="source/logo-gradle.svg" alt="Gradle">
<span>Gradle</span>
<small>Build system for Android apps</small>
</div>
<div class="tech-item">
<img src="source/logo-aws.svg" alt="AWS">
<span>AWS</span>
<small>Cloud hosting - EC2, S3, CloudFront</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/android/34A853" alt="Android">
<span>Android</span>
<small>Race Manager &amp; Buoy Pinger apps</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/apple/000000" alt="iOS">
<span>iOS / Swift</span>
<small>Sail Insight GPS tracking app</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/apachemaven/C71A36" alt="Maven">
<span>Maven</span>
<small>Build system for all Java modules</small>
</div>
<div class="tech-item tech-item--invert">
<img src="https://cdn.simpleicons.org/eclipsejetty/333333" alt="Jetty">
<span>Jetty</span>
<small>Embedded HTTP server inside OSGi</small>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════ WHAT'S NEW ════════════════════════════ -->
<section class="section" id="whatsnew">
<div class="container">
<div class="whatsnew__header">
<div>
<p class="eyebrow eyebrow--yellow">What's New</p>
<h2>Latest updates across the platform</h2>
<p class="lead" style="max-width:560px;margin-top:10px;">
Expand a product below to see its changelog.
</p>
</div>
</div>
<div id="whatsnew-sticky" class="whatsnew__sticky-bar">
<button id="whatsnew-close-btn" class="whatsnew__close-btn" onclick="closeAllAccordions()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
Close all
</button>
</div>
<div class="accordion">
<!-- Sailing Analytics -->
<div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="accordion-title">
<span>Sailing Analytics</span>
<span class="accordion-tag tag-web">Web</span>
</div>
<span class="accordion-arrow">▼</span>
</div>
<div class="accordion-body" data-src="https://raw.githubusercontent.com/eclipse-sailing-analytics/sailing-analytics/main/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html">
<div class="accordion-loading">Loading…</div>
</div>
</div>
<!-- Race Manager -->
<div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="accordion-title">
<span>Sailing Race Manager</span>
<span class="accordion-tag tag-android">Android</span>
</div>
<span class="accordion-arrow">▼</span>
</div>
<div class="accordion-body" data-src="https://raw.githubusercontent.com/eclipse-sailing-analytics/sailing-analytics/main/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/RaceCommitteeAppNotes.html">
<div class="accordion-loading">Loading…</div>
</div>
</div>
<!-- Sail Insight -->
<div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="accordion-title">
<span>Sail Insight</span>
<span class="accordion-tag tag-ios">iOS &amp; Android</span>
</div>
<span class="accordion-arrow">▼</span>
</div>
<div class="accordion-body" data-src="https://raw.githubusercontent.com/eclipse-sailing-analytics/sailing-analytics/main/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/InSightAppNotes.html">
<div class="accordion-loading">Loading…</div>
</div>
</div>
<!-- Buoy Pinger -->
<div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="accordion-title">
<span>Sailing Buoy Pinger</span>
<span class="accordion-tag tag-android">Android</span>
</div>
<span class="accordion-arrow">▼</span>
</div>
<div class="accordion-body" data-src="https://raw.githubusercontent.com/eclipse-sailing-analytics/sailing-analytics/main/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/BuoyPingerAppNotes.html">
<div class="accordion-loading">Loading…</div>
</div>
</div>
<!-- Strategy Simulator -->
<div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="accordion-title">
<span>Strategy Simulator</span>
<span class="accordion-tag tag-web">Web</span>
</div>
<span class="accordion-arrow">▼</span>
</div>
<div class="accordion-body" data-src="https://raw.githubusercontent.com/eclipse-sailing-analytics/sailing-analytics/main/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingSimulatorNotes.html">
<div class="accordion-loading">Loading…</div>
</div>
</div>
</div>
</div>
</section>
<div class="rev-modal-overlay" id="rmod-frank" onclick="closeRevModal(event, 'rmod-frank')">
<div class="rev-modal" style="max-width:760px;">
<button class="rev-modal__close" onclick="closeRevModal(null,'rmod-frank')">&times;</button>
<div class="rev-modal__photo" style="width:260px;">
<img src="source/frank_scharffe.png" alt="Frank Schärffe" style="object-position:center 12%;">
</div>
<div class="rev-modal__content">
<div class="rev-modal__quote-mark">&ldquo;</div>
<p class="rev-modal__body">With SAP's return to processing tracked data at Travemünder Woche 2026, 42 races across 9 boat classes could be processed in a graphically appealing way and presented attractively for the interested public. An excellent instrument for our participants to analyse their own performance - a genuine added value! For the sailing-enthusiast public, the data was incorporated into the live broadcast on an LED large screen and provided excellent background information for the commentary - overall, we were able to make the regatta sport of Travemünder Woche more viewer-friendly!</p>
<div class="rev-modal__footer">
<div>
<div class="rev-card__name">Frank Schärffe</div>
<div class="rev-card__role">Managing Director</div>
<div class="rev-card__org">Travemünder Woche gGmbH</div>
</div>
</div>
</div>
</div>
</div>
<div class="rev-modal-overlay" id="rmod-sven" onclick="closeRevModal(event, 'rmod-sven')">
<div class="rev-modal">
<button class="rev-modal__close" onclick="closeRevModal(null,'rmod-sven')">&times;</button>
<div class="rev-modal__photo">
<img src="source/Sven Christensen-0fc6b9f9.png" alt="Sven Christensen" style="object-position:center 15%;">
</div>
<div class="rev-modal__content">
<div class="rev-modal__quote-mark">&ldquo;</div>
<p class="rev-modal__body">SAP Sailing Analytics is an extremely helpful tool for Kieler Woche in many respects. Not only the analysis itself, but also the presentation and the depth of information behind it help us convey sailing in near real-time in a way that is broadly understandable and exciting. Beyond that, the data provides participants and coaches with valuable opportunities to analyse performance and decisions.</p>
<div class="rev-modal__footer">
<div>
<div class="rev-card__name">Sven Christensen</div>
<div class="rev-card__role">CEO, Point of Sailing - Sailing promotion &amp; staging</div>
<div class="rev-card__org">Kieler Woche</div>
</div>
</div>
</div>
</div>
</div>
<div class="rev-modal-overlay" id="rmod-stanislaw" onclick="closeRevModal(event, 'rmod-stanislaw')">
<div class="rev-modal" style="max-width:760px;">
<button class="rev-modal__close" onclick="closeRevModal(null,'rmod-stanislaw')">&times;</button>
<div class="rev-modal__photo" style="width:210px;">
<img src="source/stanislav_konarzewski.jpg" alt="Stanisław Konarzewski" style="object-position:45% 22%;">
</div>
<div class="rev-modal__content">
<div class="rev-modal__quote-mark">&ldquo;</div>
<p class="rev-modal__body">As the organizers of the Polish Sailing League, integrating SAP Sailing Analytics has been a complete game-changer for our fast-paced regattas. The system's live tracking, complete with real-time data, effortlessly translates complex tactical decisions into an engaging, easily digestible experience for our fans and commentators. In a format featuring back-to-back flights, the automated, real-time scoring is invaluable; leaderboards update instantly, eliminating manual calculation delays and ensuring absolute accuracy. From an operational standpoint, the platform is intuitive and easy to use. Deploying the GPS units across our one-design fleet adds no friction to our morning routines, while the Race Committee backend makes virtual course setting and safety monitoring seamless. Combined with the in-depth post-race analytics, SAP Sailing is much more than just a tracking tool - it is a robust, indispensable platform that has fundamentally elevated the professionalism, operational efficiency, and broadcast quality of our entire league.</p>
<div class="rev-modal__footer">
<div>
<div class="rev-card__name">Stanisław Konarzewski</div>
<div class="rev-card__role">Marketing &amp; Communication Manager</div>
<div class="rev-card__org">Polish Sailing League</div>
</div>
</div>
</div>
</div>
</div>
<div class="rev-modal-overlay" id="rmod-jack" onclick="closeRevModal(event, 'rmod-jack')">
<div class="rev-modal">
<button class="rev-modal__close" onclick="closeRevModal(null,'rmod-jack')">&times;</button>
<div class="rev-modal__photo">
<div class="rev-modal__photo-placeholder">J</div>
</div>
<div class="rev-modal__content">
<div class="rev-modal__quote-mark">&ldquo;</div>
<p class="rev-modal__body">SAP Sailing Analytics strikes a unique balance of being accessible to any average club racer on a budget, and powerful enough to satisfy the most complex regatta or performance analytics far better than its competitors. We use it for all sorts of events and are always discovering new and interesting features to take advantage of.</p>
<div class="rev-modal__footer">
<div>
<div class="rev-card__name">Jack V.</div>
<div class="rev-card__role">Pacific Northwest Sailor</div>
<div class="rev-card__org">Sidney North Saanich Yacht Club</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════ FOOTER ════════════════════════════════ -->
<footer class="footer">
<div class="container">
<div class="footer__grid">
<div class="footer__brand">
<img src="source/logo-azimuth-no-bg.png" alt="Azimuth" style="height:80px;width:auto;margin-bottom:4px;">
<p>Open-source regatta analytics platform - live GPS tracking, race analysis, and fan engagement. Proven at the Olympic Games.</p>
<div class="footer__social">
<a href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener" title="GitHub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
</a>
<a href="https://projects.eclipse.org/projects/technology.sailing-analytics" target="_blank" rel="noopener" title="Eclipse Foundation">
<img src="source/logo-eclipse.svg" alt="Eclipse Foundation" style="width:18px;height:18px;display:block;margin:auto;">
</a>
</div>
</div>
<div class="footer__col">
<h4>Product</h4>
<ul>
<li><a href="#solutions">Solutions</a></li>
<li><a href="#getting-started">Quick Start</a></li>
<li><a href="#whatsnew">What's New</a></li>
<li><a href="#reviews">Reviews</a></li>
<li><a href="https://www.sapsailing.com/gwt/Home.html" target="_blank" rel="noopener">Application</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/releases" target="_blank" rel="noopener">Releases</a></li>
</ul>
</div>
<div class="footer__col">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">GitHub</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener">Contributing</a></li>
<li><a href="https://wiki.sapsailing.com/wiki/howto/tutorials/home.md" target="_blank" rel="noopener">Support</a></li>
</ul>
</div>
<div class="footer__col">
<h4>Developers</h4>
<ul>
<li><a href="wiki.html" target="_blank" rel="noopener">Wiki</a></li>
<li><a href="https://wiki.sapsailing.com/wiki/howto/onboarding.md" target="_blank" rel="noopener">Onboarding</a></li>
<li><a href="https://wiki.sapsailing.com/wiki/info/api/api-v1.md" target="_blank" rel="noopener">API Reference</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/actions" target="_blank" rel="noopener">CI / CD</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/issues/" target="_blank" rel="noopener">Issue Tracker</a></li>
</ul>
</div>
</div>
<div class="footer__bottom">
<p>&copy; 2011&ndash;2026 Eclipse Azimuth Sailing Analytics contributors.</p>
<div class="footer__bottom-links">
<a href="https://www.sapsailing.com/gwt/Home.html#/imprint/:" target="_blank" rel="noopener">Legal Disclosure</a>
<a href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">Source Code</a>
<a href="https://wiki.sapsailing.com/wiki/howto/tutorials/home.md" target="_blank" rel="noopener">Support</a>
</div>
</div>
</div>
</footer>
<!-- ═══════════════════════════════════ MODALS ════════════════════════════════ -->
<!-- Modal: Sailing Analytics -->
<div class="modal-overlay" id="modal-sa" onclick="closeModalOnOverlay(event, 'modal-sa')">
<div class="modal">
<div class="modal__header">
<div class="modal__header-left">
<div class="modal__header-title">Eclipse Azimuth Sailing Analytics</div>
<div class="modal__header-sub">Web Platform · Cloud &amp; In-Memory</div>
</div>
<button class="modal__close" onclick="closeModal('modal-sa')">&times;</button>
</div>
<div class="modal__img-slider" id="mslider-sa">
<div class="modal__img-track">
<img src="source/sailing-analytics-Photoroom.png" alt="Eclipse Azimuth Sailing Analytics">
</div>
</div>
<div class="modal__body">
<div class="modal__cards">
<a class="modal__card" href="https://www.sapsailing.com/gwt/Home.html" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">🚀</div>
<div class="modal__card-text">
<div class="modal__card-title">Open Application</div>
<div class="modal__card-desc">sapsailing.com - real events, real data</div>
</div>
<span class="modal__card-arrow">→</span>
</a>
<a class="modal__card" href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--yellow">⭐</div>
<div class="modal__card-text">
<div class="modal__card-title">GitHub Repository</div>
<div class="modal__card-desc">Source code, issues, pull requests</div>
</div>
<span class="modal__card-arrow">→</span>
</a>
</div>
<div class="modal__section-label">Documentation</div>
<div class="modal__cards modal__cards--2">
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/tutorials/sailinganalytics/create-regattas.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">🏁</div>
<div class="modal__card-text">
<div class="modal__card-title">Create Your First Event</div>
<div class="modal__card-desc">Step-by-step regatta setup</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/tutorials/sailinganalytics/data-mining-tool.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">📊</div>
<div class="modal__card-text">
<div class="modal__card-title">Data Mining Tool</div>
<div class="modal__card-desc">Race analytics deep-dive</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/info/api/sailing-webservices.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--yellow">🔌</div>
<div class="modal__card-text">
<div class="modal__card-title">REST API Reference</div>
<div class="modal__card-desc">Web services &amp; integrations</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/onboarding.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--green">⚙️</div>
<div class="modal__card-text">
<div class="modal__card-title">Developer Onboarding</div>
<div class="modal__card-desc">Build &amp; run the project locally</div>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Modal: Race Manager -->
<div class="modal-overlay" id="modal-rm" onclick="closeModalOnOverlay(event, 'modal-rm')">
<div class="modal">
<div class="modal__header">
<div class="modal__header-left">
<div class="modal__header-title">Sailing Race Manager</div>
<div class="modal__header-sub">Android App · Race Committee Tool</div>
</div>
<button class="modal__close" onclick="closeModal('modal-rm')">&times;</button>
</div>
<div class="modal__img-slider" id="mslider-rm">
<div class="modal__img-track">
<img src="source/sailing-race-manager-1.png" alt="Race Manager 1">
<img src="source/sailing-race-manager-2.png" alt="Race Manager 2">
<img src="source/sailing-race-manager-3.png" alt="Race Manager 3">
<img src="source/sailing-race-manager-4.png" alt="Race Manager 4">
</div>
<button class="modal__img-btn modal__img-btn--prev" onclick="mSlide('mslider-rm',-1)">&#8249;</button>
<button class="modal__img-btn modal__img-btn--next" onclick="mSlide('mslider-rm',1)">&#8250;</button>
<div class="modal__img-dots" id="mdots-rm">
<button class="modal__img-dot active" onclick="mSlideTo('mslider-rm',0)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-rm',1)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-rm',2)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-rm',3)"></button>
</div>
</div>
<div class="modal__body">
<div class="modal__section-label">Download</div>
<div class="modal__store-row">
<a class="modal__store-btn" href="https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app" target="_blank" rel="noopener">
<img src="source/logo-google-play.png" width="16" height="16" alt="" style="display:inline-block;vertical-align:middle;">
Google Play
</a>
</div>
<div class="modal__section-label">Documentation</div>
<div class="modal__cards modal__cards--2">
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/tutorials/sailingracemanager/sailing-race-guide.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">📖</div>
<div class="modal__card-text">
<div class="modal__card-title">User Guide</div>
<div class="modal__card-desc">Complete race manager walkthrough</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/tutorials/sailingracemanager/configure-wind-settings.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--yellow">💨</div>
<div class="modal__card-text">
<div class="modal__card-title">Configure Wind</div>
<div class="modal__card-desc">Wind sensor setup &amp; settings</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/info/mobile/racecommittee-app.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--green">🏗️</div>
<div class="modal__card-text">
<div class="modal__card-title">App Architecture</div>
<div class="modal__card-desc">Technical design &amp; concepts</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/tutorials/sailinganalytics/sailing-race-manager.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--purple">🔗</div>
<div class="modal__card-text">
<div class="modal__card-title">Connect to Analytics</div>
<div class="modal__card-desc">Link Race Manager to the platform</div>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Modal: Sail Insight -->
<div class="modal-overlay" id="modal-si" onclick="closeModalOnOverlay(event, 'modal-si')">
<div class="modal">
<div class="modal__header">
<div class="modal__header-left">
<div class="modal__header-title">Sail Insight</div>
<div class="modal__header-sub">iOS &amp; Android · GPS Tracking for Sailors</div>
</div>
<button class="modal__close" onclick="closeModal('modal-si')">&times;</button>
</div>
<div class="modal__img-slider" id="mslider-si">
<div class="modal__img-track">
<img src="source/sail-insight-1-Photoroom.png" alt="Sail Insight 1">
<img src="source/sail-insight-2.png" alt="Sail Insight 2">
<img src="source/sail-insight-3.png" alt="Sail Insight 3">
<img src="source/sail-insight-4-Photoroom.png" alt="Sail Insight 4">
</div>
<button class="modal__img-btn modal__img-btn--prev" onclick="mSlide('mslider-si',-1)">&#8249;</button>
<button class="modal__img-btn modal__img-btn--next" onclick="mSlide('mslider-si',1)">&#8250;</button>
<div class="modal__img-dots" id="mdots-si">
<button class="modal__img-dot active" onclick="mSlideTo('mslider-si',0)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-si',1)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-si',2)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-si',3)"></button>
</div>
</div>
<div class="modal__body">
<div class="modal__section-label">Download</div>
<div class="modal__store-row">
<div class="store-split">
<a class="store-split__half" href="https://apps.apple.com/us/app/sail-insight-powered-by-sap/id1495355086" target="_blank" rel="noopener">
<img src="source/logo-app-store.png" width="15" height="15" alt="">App Store
</a>
<a class="store-split__half" href="https://play.google.com/store/apps/details?id=org.sailyachtresearch.sailinsight" target="_blank" rel="noopener">
<img src="source/logo-google-play.png" width="15" height="15" alt="">Google Play
</a>
</div>
</div>
<div class="modal__section-label">Documentation</div>
<div class="modal__cards modal__cards--2">
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/tutorials/sailinsight/getting-started.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">🚀</div>
<div class="modal__card-text">
<div class="modal__card-title">Getting Started</div>
<div class="modal__card-desc">First-time setup &amp; tracking</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/info/mobile/mobilesailinganalytics.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--purple">📱</div>
<div class="modal__card-text">
<div class="modal__card-title">Mobile Overview</div>
<div class="modal__card-desc">Architecture &amp; design goals</div>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Modal: Buoy Pinger -->
<div class="modal-overlay" id="modal-bp" onclick="closeModalOnOverlay(event, 'modal-bp')">
<div class="modal">
<div class="modal__header">
<div class="modal__header-left">
<div class="modal__header-title">Sailing Buoy Pinger</div>
<div class="modal__header-sub">Android App · Race Mark Positioning</div>
</div>
<button class="modal__close" onclick="closeModal('modal-bp')">&times;</button>
</div>
<div class="modal__img-slider" id="mslider-bp">
<div class="modal__img-track">
<img src="source/sailing-buoy-pinger-1-Photoroom.png" alt="Buoy Pinger 1">
<img src="source/sailing-buoy-pinger-2-Photoroom.png" alt="Buoy Pinger 2">
<img src="source/sailing-buoy-pinger-3-Photoroom.png" alt="Buoy Pinger 3">
</div>
<button class="modal__img-btn modal__img-btn--prev" onclick="mSlide('mslider-bp',-1)">&#8249;</button>
<button class="modal__img-btn modal__img-btn--next" onclick="mSlide('mslider-bp',1)">&#8250;</button>
<div class="modal__img-dots" id="mdots-bp">
<button class="modal__img-dot active" onclick="mSlideTo('mslider-bp',0)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-bp',1)"></button>
<button class="modal__img-dot" onclick="mSlideTo('mslider-bp',2)"></button>
</div>
</div>
<div class="modal__body">
<div class="modal__section-label">Download</div>
<div class="modal__store-row">
<a class="modal__store-btn" href="https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app" target="_blank" rel="noopener">
<img src="source/logo-google-play.png" width="16" height="16" alt="" style="display:inline-block;vertical-align:middle;">
Google Play
</a>
</div>
<div class="modal__section-label">Documentation</div>
<div class="modal__cards modal__cards--2">
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/info/mobile/smartphone-tracking-steps.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">📍</div>
<div class="modal__card-text">
<div class="modal__card-title">Tracking Setup</div>
<div class="modal__card-desc">Configure mark GPS tracking</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/info/mobile/smartphone-tracking-general.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--green">📡</div>
<div class="modal__card-text">
<div class="modal__card-title">How Tracking Works</div>
<div class="modal__card-desc">GPS signal pipeline overview</div>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Modal: Strategy Simulator -->
<div class="modal-overlay" id="modal-ss" onclick="closeModalOnOverlay(event, 'modal-ss')">
<div class="modal">
<div class="modal__header">
<div class="modal__header-left">
<div class="modal__header-title">Strategy Simulator</div>
<div class="modal__header-sub">Web Platform · Wind &amp; Current Analysis</div>
</div>
<button class="modal__close" onclick="closeModal('modal-ss')">&times;</button>
</div>
<div class="modal__img-slider" id="mslider-ss">
<div class="modal__img-track">
<img src="source/strategy-simulator-Photoroom.png" alt="Strategy Simulator">
</div>
</div>
<div class="modal__body">
<div class="modal__cards">
<a class="modal__card" href="https://www.sapsailing.com/gwt/Simulator.html" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">🧭</div>
<div class="modal__card-text">
<div class="modal__card-title">Strategy Simulator Application</div>
<div class="modal__card-desc">Live wind &amp; current strategy tool</div>
</div>
<span class="modal__card-arrow">→</span>
</a>
</div>
<div class="modal__section-label">Documentation</div>
<div class="modal__cards modal__cards--2">
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/misc/windestimation-core-concepts.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--blue">💨</div>
<div class="modal__card-text">
<div class="modal__card-title">Wind Estimation</div>
<div class="modal__card-desc">Core concepts &amp; algorithms</div>
</div>
</a>
<a class="modal__card" href="https://wiki.sapsailing.com/wiki/howto/misc/polars.md" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--yellow">⛵</div>
<div class="modal__card-text">
<div class="modal__card-title">Polar Performance Data</div>
<div class="modal__card-desc">Boat speed curves explained</div>
</div>
</a>
</div>
<div class="modal__section-label">Source Code</div>
<div class="modal__cards">
<a class="modal__card" href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">
<div class="modal__card-icon modal__card-icon--green">💻</div>
<div class="modal__card-text">
<div class="modal__card-title">GitHub Repository</div>
<div class="modal__card-desc">Simulator module in java/com.sap.sailing.simulator</div>
</div>
<span class="modal__card-arrow">→</span>
</a>
</div>
</div>
</div>
</div>
<script>
// ── Documentation categories (shared data) ────────────────────────────────
var CATS = [
{ id:'general', label:'General', icon:'🚀', color:'#008fcc',
topics:[
{label:'Onboarding', url:'https://wiki.sapsailing.com/wiki/howto/onboarding.md'},
{label:'Tutorials', url:'https://wiki.sapsailing.com/wiki/howto/tutorials/home.md'},
{label:'Project Info', url:'https://wiki.sapsailing.com/wiki/info/general/general-information.md'},
{label:'Project History', url:'https://wiki.sapsailing.com/wiki/info/general/project-history.md'},
{label:'Sailing Glossary', url:'https://wiki.sapsailing.com/wiki/howto/glossary.md'},
{label:'About Wiki', url:'https://wiki.sapsailing.com/wiki/info/general/wiki.md'},
]},
{ id:'events', label:'Event Mgrs', icon:'🏁', color:'#efaa00',
topics:[
{label:'Create Regattas', url:'https://wiki.sapsailing.com/wiki/howto/tutorials/sailinganalytics/create-regattas.md'},
{label:'Publish Event', url:'https://wiki.sapsailing.com/wiki/howto/tutorials/sailinganalytics/publish-event.md'},
{label:'Import Results', url:'https://wiki.sapsailing.com/wiki/howto/eventmanagers/results-import.md'},
{label:'WindBots', url:'https://wiki.sapsailing.com/wiki/howto/eventmanagers/windbot-operations.md'},
{label:'Media Content', url:'https://wiki.sapsailing.com/wiki/howto/eventmanagers/manage-media-content.md'},
{label:'ORC Regattas', url:'https://wiki.sapsailing.com/wiki/howto/setup-orc-regatta.md'},
]},
{ id:'mobile', label:'Mobile', icon:'📱', color:'#7c3aed',
topics:[
{label:'Sail Insight', url:'https://wiki.sapsailing.com/wiki/howto/tutorials/sailinsight/getting-started.md'},
{label:'Race Manager', url:'https://wiki.sapsailing.com/wiki/howto/tutorials/sailingracemanager/sailing-race-guide.md'},
{label:'Committee App', url:'https://wiki.sapsailing.com/wiki/info/mobile/racecommittee-app.md'},
{label:'Smartphone Track', url:'https://wiki.sapsailing.com/wiki/info/mobile/smartphone-tracking-steps.md'},
{label:'Push Notifications', url:'https://wiki.sapsailing.com/wiki/info/mobile/push-notifications.md'},
]},
{ id:'api', label:'API', icon:'🔌', color:'#16a34a',
topics:[
{label:'REST API v1', url:'https://wiki.sapsailing.com/wiki/info/api/api-v1.md'},
{label:'Web Services API', url:'https://wiki.sapsailing.com/wiki/info/api/sailing-webservices.md'},
{label:'Tracking API', url:'https://wiki.sapsailing.com/wiki/info/api/tracking-app-api.md'},
{label:'Expedition Import', url:'https://wiki.sapsailing.com/wiki/howto/expeditionimport.md'},
{label:'Wind Estimation', url:'https://wiki.sapsailing.com/wiki/howto/windestimation.md'},
]},
{ id:'infra', label:'Infra', icon:'☁️', color:'#e05a2b',
topics:[
{label:'Architecture', url:'https://wiki.sapsailing.com/wiki/info/landscape/basic-architectural-principles.md'},
{label:'Build & Deploy', url:'https://wiki.sapsailing.com/wiki/info/landscape/building-and-deploying.md'},
{label:'Runtime Env', url:'https://wiki.sapsailing.com/wiki/info/landscape/runtime-environment.md'},
{label:'Amazon EC2', url:'https://wiki.sapsailing.com/wiki/info/landscape/amazon-ec2.md'},
{label:'Docker Registry', url:'https://wiki.sapsailing.com/wiki/info/landscape/docker-registry.md'},
{label:'Server Replication', url:'https://wiki.sapsailing.com/wiki/howto/misc/server-replication.md'},
]},
{ id:'dev', label:'Development', icon:'⚙️', color:'#0891b2',
topics:[
{label:'Dev Environment', url:'https://wiki.sapsailing.com/wiki/info/landscape/development-environment.md'},
{label:'Dev Scenarios', url:'https://wiki.sapsailing.com/wiki/info/landscape/typical-development-scenarios.md'},
{label:'GWT Super Dev', url:'https://wiki.sapsailing.com/wiki/howto/development/super-dev-mode.md'},
{label:'CI / Jenkins', url:'https://wiki.sapsailing.com/wiki/howto/development/ci.md'},
{label:'JVM Profiling', url:'https://wiki.sapsailing.com/wiki/howto/development/profiling.md'},
{label:'AI Agent', url:'https://wiki.sapsailing.com/wiki/info/landscape/ai-agent.md'},
]},
];
// ── Solutions carousel ────────────────────────────────────────────────────
(function() {
const track = document.getElementById('sol-track');
const dotsEl = document.getElementById('sol-dots');
const cards = Array.from(track.querySelectorAll('.sol-card'));
const n = cards.length;
let cur = 0;
let visCount = 3;
const GAP = 20;
function getVis() {
return window.innerWidth < 640 ? 1 : 2;
}
function cardWidth() {
return cards[0].offsetWidth;
}
function pages() {
return n - visCount + 1;
}
function buildDots() {
dotsEl.innerHTML = '';
for (let i = 0; i < pages(); i++) {
const d = document.createElement('button');
d.className = 'solutions__dot' + (i === 0 ? ' active' : '');
(function(idx) { d.onclick = function() { goto(idx); }; })(i);
dotsEl.appendChild(d);
}
}
function goto(idx) {
cur = Math.max(0, Math.min(idx, pages() - 1));
track.style.transform = 'translateX(-' + (cur * (cardWidth() + GAP)) + 'px)';
dotsEl.querySelectorAll('.solutions__dot').forEach(function(d, i) {
d.classList.toggle('active', i === cur);
});
document.getElementById('sol-prev').disabled = cur === 0;
document.getElementById('sol-next').disabled = cur >= pages() - 1;
}
window.solNav = function(dir) { goto(cur + dir); };
// Click anywhere on card opens modal (except clicking actual buttons/links)
cards.forEach(function(card) {
card.style.cursor = 'pointer';
card.addEventListener('click', function(e) {
if (e.target.closest('button') || e.target.closest('a')) return;
const btn = card.querySelector('button.btn-primary');
if (btn) btn.click();
});
});
// Drag / swipe
let dragStart = null;
track.addEventListener('mousedown', function(e) { dragStart = e.clientX; track.classList.add('dragging'); });
track.addEventListener('touchstart', function(e) { dragStart = e.touches[0].clientX; }, {passive:true});
function dragEnd(x) {
if (dragStart === null) return;
track.classList.remove('dragging');
const dx = dragStart - x;
if (Math.abs(dx) > 40) goto(cur + (dx > 0 ? 1 : -1));
dragStart = null;
}
track.addEventListener('mouseup', function(e) { dragEnd(e.clientX); });
track.addEventListener('touchend', function(e) { dragEnd(e.changedTouches[0].clientX); });
track.addEventListener('mouseleave', function(e) { if (dragStart !== null) dragEnd(e.clientX); });
function init() {
visCount = getVis();
buildDots();
goto(0);
}
// Wait for layout before first goto so offsetWidth is valid
if (document.readyState === 'complete') { init(); }
else { window.addEventListener('load', init); }
window.addEventListener('resize', function() { init(); });
})();
// ── Modal image slider ────────────────────────────────────────────────────
var mSliderState = {};
function mSlideTo(id, idx) {
const el = document.getElementById(id);
const track = el.querySelector('.modal__img-track');
const imgs = track.querySelectorAll('img');
const n = imgs.length;
idx = ((idx % n) + n) % n;
mSliderState[id] = idx;
track.style.transform = 'translateX(-' + (idx * 100) + '%)';
const dots = document.getElementById('mdots-' + id.replace('mslider-', ''));
if (dots) dots.querySelectorAll('.modal__img-dot').forEach(function(d, i) {
d.classList.toggle('active', i === idx);
});
}
function mSlide(id, dir) { mSlideTo(id, (mSliderState[id] || 0) + dir); }
// ── Documentation card grid ──────────────────────────────────────────────
(function() {
const grid = document.getElementById('docs-grid');
if (!grid) return;
CATS.forEach(function(cat) {
const card = document.createElement('div');
card.className = 'docs__card';
const headBg = cat.color + '1a'; // 10% opacity tint
card.innerHTML =
'<div class="docs__card-head">' +
'<div class="docs__card-icon" style="background:' + headBg + ';color:' + cat.color + '">' + cat.icon + '</div>' +
'<h3>' + cat.label + '</h3>' +
'</div>' +
'<div class="docs__card-links">' +
cat.topics.map(function(t) {
return '<a class="docs__link" href="' + t.url + '" target="_blank" rel="noopener">' +
'<span>' + t.label + '</span>' +
'<span class="docs__link-arrow" style="color:' + cat.color + '">→</span>' +
'</a>';
}).join('') +
'</div>';
grid.appendChild(card);
});
})();
// Accordion: lazy-load source HTML on first open
function toggleAccordion(header) {
const body = header.nextElementSibling;
const isOpen = header.classList.contains('open');
header.classList.toggle('open', !isOpen);
body.classList.toggle('open', !isOpen);
updateAccordionFab();
if (!isOpen && body.dataset.src && !body.dataset.loaded) {
body.dataset.loaded = '1';
fetch(body.dataset.src)
.then(function(r) { return r.ok ? r.text() : Promise.reject(r.status); })
.then(function(html) {
// Extract the innerContent div from the source file
const tmp = document.createElement('div');
tmp.innerHTML = html;
const inner = tmp.querySelector('.innerContent') || tmp.querySelector('#mainContent') || tmp;
const div = document.createElement('div');
div.className = 'whatsnew-content';
div.innerHTML = inner.innerHTML;
body.innerHTML = '';
body.appendChild(div);
})
.catch(function() {
var div = document.createElement('div');
div.className = 'accordion-loading';
div.appendChild(document.createTextNode('Could not load changelog — '));
var a = document.createElement('a');
a.href = body.dataset.src;
a.target = '_blank';
a.textContent = 'view source file';
div.appendChild(a);
body.innerHTML = '';
body.appendChild(div);
});
}
}
// Close-all button: fixed position, visible only while #whatsnew is in viewport AND an accordion is open
var whatsnewVisible = false;
(function() {
var section = document.getElementById('whatsnew');
if (!section) return;
var obs = new IntersectionObserver(function(entries) {
whatsnewVisible = entries[0].isIntersecting;
updateAccordionFab();
}, { threshold: 0 });
obs.observe(section);
})();
function updateAccordionFab() {
var anyOpen = document.querySelector('.accordion-header.open') !== null;
document.getElementById('whatsnew-sticky').classList.toggle('visible', anyOpen && whatsnewVisible);
}
function closeAllAccordions() {
document.querySelectorAll('.accordion-header.open').forEach(function(h) {
h.classList.remove('open');
h.nextElementSibling.classList.remove('open');
});
updateAccordionFab();
}
function openModal(id) {
document.getElementById(id).classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeModal(id) {
document.getElementById(id).classList.remove('open');
document.body.style.overflow = '';
}
function closeModalOnOverlay(event, id) {
if (event.target === document.getElementById(id)) {
closeModal(id);
}
}
function openRevModal(id) {
document.getElementById(id).classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeRevModal(event, id) {
if (event && event.target !== document.getElementById(id)) return;
document.getElementById(id).classList.remove('open');
document.body.style.overflow = '';
}
// Show fade + chevron only when text actually overflows
window.addEventListener('load', function() {
document.querySelectorAll('.rev-card').forEach(function(card) {
var wrap = card.querySelector('.rev-card__body-wrap');
var body = card.querySelector('.rev-card__body');
if (wrap && body && body.scrollHeight > wrap.clientHeight + 4) {
wrap.classList.add('overflows');
}
});
});
(function() {
var wrap = document.querySelector('.reviews__ticker-wrap');
if (!wrap) return;
var isDragging = false, startX, scrollLeft;
function wrapScroll() {
var ticker = document.getElementById('rev-ticker');
if (!ticker) return;
var half = ticker.scrollWidth / 2;
if (wrap.scrollLeft >= half) wrap.scrollLeft -= half;
else if (wrap.scrollLeft < 0) wrap.scrollLeft += half;
}
wrap.addEventListener('mousedown', function(e) {
isDragging = true;
startX = e.pageX - wrap.offsetLeft;
scrollLeft = wrap.scrollLeft;
wrap.style.userSelect = 'none';
});
document.addEventListener('mouseup', function() {
isDragging = false;
wrap.style.userSelect = '';
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
e.preventDefault();
var x = e.pageX - wrap.offsetLeft;
wrap.scrollLeft = scrollLeft - (x - startX);
wrapScroll();
});
wrap.addEventListener('scroll', wrapScroll);
})();
function revTickerStep(dir) {
var wrap = document.querySelector('.reviews__ticker-wrap');
var ticker = document.getElementById('rev-ticker');
if (!wrap || !ticker) return;
var card = wrap.querySelector('.rev-card');
var cardW = card ? card.offsetWidth + 24 : 364;
var totalW = ticker.scrollWidth / 2;
wrap.scrollLeft += dir * cardW;
// wrap around
if (wrap.scrollLeft <= 0 && dir < 0) {
wrap.scrollLeft = totalW + wrap.scrollLeft;
} else if (wrap.scrollLeft >= totalW && dir > 0) {
wrap.scrollLeft = wrap.scrollLeft - totalW;
}
}
(function() {
var ticker = document.getElementById('rev-ticker');
if (!ticker) return;
var origCards = Array.from(ticker.querySelectorAll('.rev-card'));
origCards.forEach(function(c) {
var clone = c.cloneNode(true);
clone.setAttribute('aria-hidden', 'true');
ticker.appendChild(clone);
});
// start at middle so both prev and next work immediately
var wrap = document.querySelector('.reviews__ticker-wrap');
if (wrap) wrap.scrollLeft = 0;
})();
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.open, .rev-modal-overlay.open').forEach(function(m) {
m.classList.remove('open');
});
document.body.style.overflow = '';
}
});
</script>
</body>
</html>