From 3285960ee3a0c82f8514caac31b861343da8d90c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Jul 2026 22:27:20 +0000 Subject: [PATCH] chore: rebuild website [skip ci] --- website/_site/about/index.html | 32 +- website/_site/assets/css/main.css | 247 +++- website/_site/assets/js/hero-radio-fx.js | 1216 +++++------------ website/_site/contact/index.html | 32 +- website/_site/download/index.html | 32 +- website/_site/faq/index.html | 32 +- website/_site/features/airscout/index.html | 32 +- website/_site/features/dual-chat/index.html | 32 +- website/_site/features/dx-cluster/index.html | 32 +- website/_site/features/index.html | 32 +- website/_site/features/log-sync/index.html | 32 +- website/_site/features/macros/index.html | 32 +- .../_site/features/priority-score/index.html | 32 +- .../_site/features/sked-reminder/index.html | 32 +- website/_site/features/timeline/index.html | 32 +- website/_site/index.html | 46 +- website/_site/legal-notice/index.html | 32 +- .../manual/de/airscout-integration/index.html | 32 +- .../manual/de/benutzeroberflaeche/index.html | 32 +- website/_site/manual/de/changelog/index.html | 32 +- .../manual/de/dx-cluster-server/index.html | 32 +- website/_site/manual/de/funktionen/index.html | 32 +- website/_site/manual/de/home/index.html | 32 +- website/_site/manual/de/index.html | 32 +- .../_site/manual/de/installation/index.html | 32 +- .../_site/manual/de/konfiguration/index.html | 32 +- .../manual/de/log-synchronisation/index.html | 32 +- .../manual/de/makros-und-variablen/index.html | 32 +- .../manual/en/airscout-integration/index.html | 32 +- website/_site/manual/en/changelog/index.html | 32 +- .../_site/manual/en/configuration/index.html | 32 +- .../manual/en/dx-cluster-server/index.html | 32 +- website/_site/manual/en/features/index.html | 32 +- website/_site/manual/en/home/index.html | 32 +- website/_site/manual/en/index.html | 32 +- .../_site/manual/en/installation/index.html | 32 +- website/_site/manual/en/log-sync/index.html | 32 +- .../manual/en/macros-and-variables/index.html | 32 +- .../_site/manual/en/user-interface/index.html | 32 +- website/_site/manual/index.html | 32 +- .../index.html | 32 +- .../news/2026-07-website-launch/index.html | 32 +- website/_site/news/index.html | 32 +- website/_site/privacy/index.html | 32 +- website/_site/roadmap/index.html | 32 +- website/_site/screenshots/index.html | 32 +- website/_site/support/index.html | 32 +- 47 files changed, 1165 insertions(+), 1752 deletions(-) diff --git a/website/_site/about/index.html b/website/_site/about/index.html index d3d8731..ec98e35 100644 --- a/website/_site/about/index.html +++ b/website/_site/about/index.html @@ -4,6 +4,7 @@ About KST4Contest + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

About

Built for real contest operation.

@@ -103,8 +102,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/assets/css/main.css b/website/_site/assets/css/main.css index 2031449..4e5d0cb 100644 --- a/website/_site/assets/css/main.css +++ b/website/_site/assets/css/main.css @@ -11,18 +11,6 @@ --accent2: #a855f7; --accent3: #22c55e; --shadow: 0 24px 80px rgba(0, 0, 0, 0.45); - - --accent: #54d63d; - --accent2: #8aef64; - --accent3: #31b54a; - - --fx-plane: #79ff73; - --fx-plane-muted: rgba(121, 255, 115, 0.42); - --fx-beam: #52ff65; - --fx-beam-glow: rgba(82, 255, 101, 0.28); - --fx-reflection: #d2ffc8; - --fx-trail: rgba(121, 255, 115, 0.17); - --fx-grid: rgba(121, 255, 115, 0.055); } * { @@ -37,7 +25,6 @@ html { body { margin: 0; min-height: 100vh; - background: radial-gradient(circle at 14% 12%, rgba(168, 85, 247, 0.30), transparent 28%), radial-gradient(circle at 82% 16%, rgba(56, 189, 248, 0.24), transparent 30%), @@ -575,52 +562,246 @@ a:hover { grid-template-columns: 1fr; } } +/* ================================================================ + KST4Contest brand recovery: green operator-console identity + and full-screen interactive hero + ================================================================ */ -/* - * Full-width, full-height home hero - */ +:root { + --bg: #050806; + --bg2: #0b140f; + --panel: rgba(15, 28, 20, 0.82); + --panel2: rgba(24, 43, 31, 0.84); + --border: rgba(125, 178, 134, 0.22); + --text: #f4f7f3; + --muted: #a9b8ad; + --soft: #6e8173; + --accent: #54d63d; + --accent2: #8aef64; + --accent3: #31b54a; + --fx-plane: #79ff73; + --fx-plane-muted: rgba(121, 255, 115, 0.42); + --fx-beam: #52ff65; + --fx-beam-glow: rgba(82, 255, 101, 0.28); + --fx-reflection: #d2ffc8; + --fx-trail: rgba(121, 255, 115, 0.17); + --fx-grid: rgba(121, 255, 115, 0.055); +} + +body { + background: + radial-gradient(circle at 14% 12%, rgba(52, 165, 57, 0.20), transparent 30%), + radial-gradient(circle at 82% 16%, rgba(95, 218, 82, 0.14), transparent 32%), + radial-gradient(circle at 50% 85%, rgba(32, 126, 55, 0.12), transparent 36%), + linear-gradient(180deg, #070b09 0%, #0b1410 100%); +} + +body::before { + background: + radial-gradient(circle at 20% 20%, rgba(60, 173, 61, 0.18), transparent 28%), + radial-gradient(circle at 78% 18%, rgba(97, 222, 80, 0.13), transparent 30%), + radial-gradient(circle at 45% 80%, rgba(34, 122, 53, 0.11), transparent 26%); +} + +body::after { + background-image: + linear-gradient(rgba(160,255,160,0.032) 1px, transparent 1px), + linear-gradient(90deg, rgba(160,255,160,0.032) 1px, transparent 1px); +} + +.site-bg { + background: + linear-gradient(180deg, rgba(5, 8, 6, 0.10), rgba(5, 8, 6, 0.84)), + radial-gradient(circle at 8% 24%, rgba(52, 165, 57, 0.14), transparent 26%), + radial-gradient(circle at 82% 20%, rgba(95, 218, 82, 0.12), transparent 30%); +} + +.site-header { + background: rgba(5, 8, 7, 0.82); +} + +.brand-mark, +.nav-cta, +.button { + background: linear-gradient(135deg, var(--accent), var(--accent2)); +} + +.brand-mark { + box-shadow: + 0 0 26px rgba(84, 214, 61, 0.42), + 0 0 50px rgba(49, 181, 74, 0.16); +} + +.nav-cta { + color: #061008 !important; + box-shadow: 0 0 22px rgba(84, 214, 61, 0.28); +} main { width: 100%; } .hero.hero-split { + position: relative; width: 100%; max-width: none; - min-height: calc(100vh - 77px); - padding: - clamp(56px, 7vh, 100px) - clamp(28px, 6vw, 110px); + min-height: calc(100vh - 73px); + display: grid; + grid-template-columns: minmax(420px, 0.9fr) minmax(520px, 1.1fr); + gap: clamp(40px, 5vw, 110px); + align-items: center; align-content: center; + padding: clamp(56px, 7vh, 100px) clamp(28px, 6vw, 110px); overflow: hidden; } -.hero.hero-split > div:first-of-type { +.hero.hero-split > *:not(.hero-radio-fx):not(.hero-fx-vignette) { + position: relative; + z-index: 3; +} + +.hero-copy { max-width: 760px; } -.hero.hero-split .hero-panel { - width: min(100%, 700px); +.hero.hero-split h1 { + font-size: clamp(3.4rem, 8vw, 7rem); + background: linear-gradient(90deg, #ffffff 0%, #c9ffd0 28%, #77ec70 58%, #45bd4d 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.badge, +.eyebrow { + color: #caffc0; + background: rgba(52, 165, 57, 0.12); + border-color: rgba(105, 232, 88, 0.32); + box-shadow: 0 0 22px rgba(82, 255, 101, 0.08); +} + +.button { + color: #041007; + box-shadow: + 0 16px 44px rgba(84, 214, 61, 0.22), + inset 0 1px 0 rgba(255,255,255,0.35); +} + +.button.secondary { + color: white; + background: rgba(28, 43, 32, 0.90); +} + +.hero-panel { + width: min(100%, 760px); justify-self: center; + transform: perspective(1200px) rotateY(-4deg) rotateX(1.5deg); +} + +.hero-panel, +.card, +.cta-panel { + background: + linear-gradient(180deg, rgba(255,255,255,0.045), transparent 42%), + rgba(15, 28, 20, 0.82); +} + +.mini-card { + background: rgba(12, 25, 17, 0.88); + border-color: rgba(148, 184, 155, 0.16); +} + +.card:hover { + border-color: rgba(84, 214, 61, 0.42); + background: + linear-gradient(180deg, rgba(255,255,255,0.055), transparent 42%), + rgba(12, 25, 17, 0.94); + box-shadow: + 0 26px 90px rgba(0,0,0,0.50), + 0 0 30px rgba(84,214,61,0.07); +} + +.hero-radio-fx { + position: absolute; + inset: 0; + z-index: 0; + width: 100%; + height: 100%; + pointer-events: none; + opacity: 0.78; +} + +.hero-fx-vignette { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: + linear-gradient(90deg, + rgba(7,11,9,0.92) 0%, + rgba(7,11,9,0.70) 32%, + rgba(7,11,9,0.17) 62%, + rgba(7,11,9,0.38) 100%), + linear-gradient(180deg, + rgba(7,11,9,0.08), + rgba(7,11,9,0.58)); +} + +html[data-theme="light"] { + --bg: #f4f4f2; + --bg2: #ffffff; + --panel: rgba(255,255,255,0.90); + --panel2: rgba(242,242,239,0.94); + --border: rgba(91,91,86,0.20); + --text: #252522; + --muted: #656560; + --soft: #888881; + --accent: #d33732; + --accent2: #f25a51; + --accent3: #ae2727; + --fx-plane: #df3732; + --fx-plane-muted: rgba(211,55,50,0.38); + --fx-beam: #ef4039; + --fx-beam-glow: rgba(239,64,57,0.22); + --fx-reflection: #ff8d84; + --fx-trail: rgba(211,55,50,0.15); + --fx-grid: rgba(185,47,43,0.045); } @media (min-width: 1600px) { .hero.hero-split { - grid-template-columns: - minmax(520px, 0.85fr) - minmax(600px, 1.15fr); - gap: clamp(50px, 6vw, 130px); + grid-template-columns: minmax(520px, 0.85fr) minmax(620px, 1.15fr); } - .hero.hero-split .hero-panel { - width: min(100%, 780px); + .hero-panel { + width: min(100%, 820px); } } -@media (max-width: 900px) { +@media (max-width: 1000px) { .hero.hero-split { min-height: auto; - padding-top: 60px; - padding-bottom: 60px; + grid-template-columns: 1fr; + padding-top: 64px; + padding-bottom: 64px; } -} \ No newline at end of file + + .hero-panel { + width: min(100%, 760px); + transform: none; + } + + .hero-radio-fx { + opacity: 0.50; + } + + .hero-fx-vignette { + background: linear-gradient(180deg, rgba(7,11,9,0.84), rgba(7,11,9,0.34)); + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-radio-fx { + opacity: 0.28; + } +} diff --git a/website/_site/assets/js/hero-radio-fx.js b/website/_site/assets/js/hero-radio-fx.js index 7c4a292..7cb2787 100644 --- a/website/_site/assets/js/hero-radio-fx.js +++ b/website/_site/assets/js/hero-radio-fx.js @@ -2,33 +2,14 @@ "use strict"; const canvas = document.querySelector("[data-hero-radio-fx]"); - - if (!(canvas instanceof HTMLCanvasElement)) { - return; - } - - const context = canvas.getContext("2d", { - alpha: true, - desynchronized: true - }); - - if (!context) { - return; - } + if (!(canvas instanceof HTMLCanvasElement)) return; const hero = canvas.closest(".hero"); + const ctx = canvas.getContext("2d", { alpha: true, desynchronized: true }); + if (!(hero instanceof HTMLElement) || !ctx) return; - if (!(hero instanceof HTMLElement)) { - return; - } - - const reducedMotion = window.matchMedia( - "(prefers-reduced-motion: reduce)" - ); - - const coarsePointer = window.matchMedia( - "(pointer: coarse)" - ); + const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)"); + const coarsePointer = matchMedia("(pointer: coarse)"); const state = { width: 0, @@ -37,967 +18,492 @@ running: false, visible: true, lastFrame: 0, - spawnAccumulator: 0, - beamAccumulator: 0, - animationFrameId: 0, - pointer: { - x: 0, - y: 0, - active: false - }, + spawnTime: 0, + beamTime: 0, + frameId: 0, + pointer: { x: 0, y: 0, active: false }, planes: [], beams: [], reflections: [] }; - const clamp = (value, minimum, maximum) => - Math.max(minimum, Math.min(maximum, value)); + const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); + const random = (min, max) => min + Math.random() * (max - min); - const randomBetween = (minimum, maximum) => - minimum + Math.random() * (maximum - minimum); - - function getCssColor(name, fallback) { - const value = getComputedStyle(document.documentElement) - .getPropertyValue(name) - .trim(); - - return value || fallback; + function css(name, fallback) { + return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; } - function getPalette() { + function palette() { return { - plane: getCssColor("--fx-plane", "#79ff73"), - planeMuted: getCssColor("--fx-plane-muted", "rgba(121,255,115,.42)"), - beam: getCssColor("--fx-beam", "#52ff65"), - beamGlow: getCssColor("--fx-beam-glow", "rgba(82,255,101,.28)"), - reflection: getCssColor("--fx-reflection", "#c8ffbf"), - trail: getCssColor("--fx-trail", "rgba(121,255,115,.16)"), - grid: getCssColor("--fx-grid", "rgba(121,255,115,.055)") + plane: css("--fx-plane", "#79ff73"), + planeMuted: css("--fx-plane-muted", "rgba(121,255,115,.42)"), + beam: css("--fx-beam", "#52ff65"), + beamGlow: css("--fx-beam-glow", "rgba(82,255,101,.28)"), + reflection: css("--fx-reflection", "#d2ffc8"), + trail: css("--fx-trail", "rgba(121,255,115,.17)"), + grid: css("--fx-grid", "rgba(121,255,115,.055)") }; } - function resizeCanvas() { - const bounds = hero.getBoundingClientRect(); - - state.width = Math.max(1, Math.round(bounds.width)); - state.height = Math.max(1, Math.round(bounds.height)); - state.dpr = Math.min(window.devicePixelRatio || 1, 1.5); + function roundedRect(x, y, width, height, radius) { + const r = Math.min(radius, width / 2, height / 2); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + width, y, x + width, y + height, r); + ctx.arcTo(x + width, y + height, x, y + height, r); + ctx.arcTo(x, y + height, x, y, r); + ctx.arcTo(x, y, x + width, y, r); + ctx.closePath(); + } + function resize() { + const rect = hero.getBoundingClientRect(); + state.width = Math.max(1, Math.round(rect.width)); + state.height = Math.max(1, Math.round(rect.height)); + state.dpr = Math.min(devicePixelRatio || 1, 1.5); canvas.width = Math.round(state.width * state.dpr); canvas.height = Math.round(state.height * state.dpr); - canvas.style.width = `${state.width}px`; canvas.style.height = `${state.height}px`; - - context.setTransform( - state.dpr, - 0, - 0, - state.dpr, - 0, - 0 - ); + ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0); if (!state.pointer.active) { - state.pointer.x = state.width * 0.54; + state.pointer.x = state.width * 0.55; state.pointer.y = state.height * 0.42; } } - function getPlaneLimit() { - if (coarsePointer.matches) { - return 5; - } + function stations() { + const targetX = state.width * 0.54; + const targetY = state.height * 0.43; + const left = { x: state.width * 0.075, y: state.height * 0.82, label: "JN49EM" }; + const right = { x: state.width * 0.925, y: state.height * 0.80, label: "JO66MD" }; + left.angle = Math.atan2(targetY - left.y, targetX - left.x); + right.angle = Math.atan2(targetY - right.y, targetX - right.x); + return [left, right]; + } - if (state.width < 900) { - return 7; - } - - return 12; + function planeLimit() { + if (coarsePointer.matches) return 5; + return state.width < 900 ? 7 : 12; } function createPlane() { const margin = 55; const edge = Math.floor(Math.random() * 4); - let x; let y; - switch (edge) { - case 0: - x = randomBetween(0, state.width); - y = -margin; - break; - - case 1: - x = state.width + margin; - y = randomBetween(0, state.height); - break; - - case 2: - x = randomBetween(0, state.width); - y = state.height + margin; - break; - - default: - x = -margin; - y = randomBetween(0, state.height); - break; + if (edge === 0) { + x = random(0, state.width); + y = -margin; + } else if (edge === 1) { + x = state.width + margin; + y = random(0, state.height); + } else if (edge === 2) { + x = random(0, state.width); + y = state.height + margin; + } else { + x = -margin; + y = random(0, state.height); } - const targetX = state.pointer.active - ? state.pointer.x - : state.width * randomBetween(0.38, 0.72); - - const targetY = state.pointer.active - ? state.pointer.y - : state.height * randomBetween(0.2, 0.72); - - const angle = Math.atan2(targetY - y, targetX - x); - const speed = randomBetween(18, 34); + const tx = state.pointer.active ? state.pointer.x : state.width * random(0.38, 0.72); + const ty = state.pointer.active ? state.pointer.y : state.height * random(0.20, 0.72); + const angle = Math.atan2(ty - y, tx - x); + const speed = random(18, 34); return { - x, - y, - previousX: x, - previousY: y, - velocityX: Math.cos(angle) * speed, - velocityY: Math.sin(angle) * speed, - angle, - size: randomBetween(5.5, 9), + x, y, angle, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + size: random(5.5, 9), age: 0, - maximumAge: randomBetween(12, 20), - steering: randomBetween(0.65, 1.2), - wobble: randomBetween(0, Math.PI * 2), - wobbleSpeed: randomBetween(0.7, 1.4), + maxAge: random(12, 20), + steering: random(0.65, 1.2), + wobble: random(0, Math.PI * 2), + wobbleSpeed: random(0.7, 1.4), hit: 0 }; } - function updatePlane(plane, deltaSeconds) { - plane.age += deltaSeconds; - plane.wobble += deltaSeconds * plane.wobbleSpeed; + function updatePlane(plane, dt) { + plane.age += dt; + plane.wobble += dt * plane.wobbleSpeed; - plane.previousX = plane.x; - plane.previousY = plane.y; - - const targetX = state.pointer.active - ? state.pointer.x - : state.width * 0.56; - - const targetY = state.pointer.active - ? state.pointer.y - : state.height * 0.44; - - const desiredAngle = Math.atan2( - targetY - plane.y, - targetX - plane.x - ); - - let angleDifference = desiredAngle - plane.angle; - - while (angleDifference > Math.PI) { - angleDifference -= Math.PI * 2; - } - - while (angleDifference < -Math.PI) { - angleDifference += Math.PI * 2; - } - - plane.angle += angleDifference * plane.steering * deltaSeconds; - - const speed = Math.hypot( - plane.velocityX, - plane.velocityY - ); + const tx = state.pointer.active ? state.pointer.x : state.width * 0.56; + const ty = state.pointer.active ? state.pointer.y : state.height * 0.44; + const desired = Math.atan2(ty - plane.y, tx - plane.x); + let diff = desired - plane.angle; + while (diff > Math.PI) diff -= Math.PI * 2; + while (diff < -Math.PI) diff += Math.PI * 2; + plane.angle += diff * plane.steering * dt; + const speed = Math.hypot(plane.vx, plane.vy); const wobble = Math.sin(plane.wobble) * 0.14; - - plane.velocityX = - Math.cos(plane.angle + wobble) * speed; - - plane.velocityY = - Math.sin(plane.angle + wobble) * speed; - - plane.x += plane.velocityX * deltaSeconds; - plane.y += plane.velocityY * deltaSeconds; - - plane.hit = Math.max(0, plane.hit - deltaSeconds * 1.8); + plane.vx = Math.cos(plane.angle + wobble) * speed; + plane.vy = Math.sin(plane.angle + wobble) * speed; + plane.x += plane.vx * dt; + plane.y += plane.vy * dt; + plane.hit = Math.max(0, plane.hit - dt * 1.8); } - function isPlaneExpired(plane) { + function expired(plane) { const margin = 180; - - return ( - plane.age > plane.maximumAge || - plane.x < -margin || - plane.x > state.width + margin || - plane.y < -margin || - plane.y > state.height + margin - ); + return plane.age > plane.maxAge || plane.x < -margin || plane.x > state.width + margin || plane.y < -margin || plane.y > state.height + margin; } - function chooseBeamTarget() { - if (state.planes.length === 0) { - return null; - } - - const targetX = state.pointer.active - ? state.pointer.x - : state.width * 0.56; - - const targetY = state.pointer.active - ? state.pointer.y - : state.height * 0.44; - - const candidates = state.planes - .map((plane) => ({ - plane, - distance: Math.hypot( - plane.x - targetX, - plane.y - targetY - ) - })) - .filter((entry) => entry.distance < 260) + function beamTarget() { + if (!state.planes.length) return null; + const tx = state.pointer.active ? state.pointer.x : state.width * 0.56; + const ty = state.pointer.active ? state.pointer.y : state.height * 0.44; + const nearby = state.planes + .map(plane => ({ plane, distance: Math.hypot(plane.x - tx, plane.y - ty) })) + .filter(item => item.distance < 280) .sort((a, b) => a.distance - b.distance); - - if (candidates.length > 0) { - return candidates[0].plane; - } - - return state.planes[ - Math.floor(Math.random() * state.planes.length) - ]; + return nearby[0]?.plane || state.planes[Math.floor(Math.random() * state.planes.length)]; } - function getRadioStations() { - const centerX = state.width * 0.54; - const centerY = state.height * 0.43; - - const left = { - x: state.width * 0.075, - y: state.height * 0.82, - label: "JN49EM" - }; - - const right = { - x: state.width * 0.925, - y: state.height * 0.80, - label: "JO66MD" - }; - - left.angle = Math.atan2( - centerY - left.y, - centerX - left.x - ); - - right.angle = Math.atan2( - centerY - right.y, - centerX - right.x - ); - - return [left, right]; - } - - function createBeam(target) {function createBeam(target) { - const stations = getRadioStations(); - - const source = stations - .map((station) => ({ - station, - distance: Math.hypot( - target.x - station.x, - target.y - station.y - ) - })) + function createBeam(target) { + const source = stations() + .map(station => ({ station, distance: Math.hypot(target.x - station.x, target.y - station.y) })) .sort((a, b) => a.distance - b.distance)[0].station; - const beamAngle = Math.atan2( - target.y - source.y, - target.x - source.x - ); - - /* - * Der Strahl beginnt nicht im Antennenmast, - * sondern etwas vor der Yagi. - */ - const antennaTipDistance = 45; - - const startX = - source.x + - Math.cos(beamAngle) * antennaTipDistance; - - const startY = - source.y + - Math.sin(beamAngle) * antennaTipDistance; - + const angle = Math.atan2(target.y - source.y, target.x - source.x); + const tip = 45; state.beams.push({ - startX, - startY, + startX: source.x + Math.cos(angle) * tip, + startY: source.y + Math.sin(angle) * tip, endX: target.x, endY: target.y, age: 0, - maximumAge: 0.72 + maxAge: 0.72 }); target.hit = 1; - - const probabilities = [50, 75, 100]; - const probability = - probabilities[ - Math.floor(Math.random() * probabilities.length) - ]; - + const values = [50, 75, 100]; state.reflections.push({ x: target.x, y: target.y, age: 0, - maximumAge: 1.25, - radius: randomBetween(8, 13), - probability + maxAge: 1.25, + radius: random(8, 13), + probability: values[Math.floor(Math.random() * values.length)] }); } - function updateEffects(deltaSeconds) { - for (const beam of state.beams) { - beam.age += deltaSeconds; - } - - state.beams = state.beams.filter( - (beam) => beam.age < beam.maximumAge - ); - - for (const reflection of state.reflections) { - reflection.age += deltaSeconds; - reflection.radius += deltaSeconds * 30; - } - - state.reflections = state.reflections.filter( - (reflection) => - reflection.age < reflection.maximumAge - ); + function updateEffects(dt) { + state.beams.forEach(beam => beam.age += dt); + state.beams = state.beams.filter(beam => beam.age < beam.maxAge); + state.reflections.forEach(r => { + r.age += dt; + r.radius += dt * 30; + }); + state.reflections = state.reflections.filter(r => r.age < r.maxAge); } - function drawGrid(palette) { - context.save(); - - context.strokeStyle = palette.grid; - context.lineWidth = 1; - + function drawGrid(p) { + ctx.save(); + ctx.strokeStyle = p.grid; + ctx.lineWidth = 1; const spacing = 54; - - for ( - let x = spacing; - x < state.width; - x += spacing - ) { - context.beginPath(); - context.moveTo(x, 0); - context.lineTo(x, state.height); - context.stroke(); + for (let x = spacing; x < state.width; x += spacing) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, state.height); + ctx.stroke(); } - - for ( - let y = spacing; - y < state.height; - y += spacing - ) { - context.beginPath(); - context.moveTo(0, y); - context.lineTo(state.width, y); - context.stroke(); + for (let y = spacing; y < state.height; y += spacing) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(state.width, y); + ctx.stroke(); } - - context.restore(); + ctx.restore(); } - function drawRadarTarget(palette) { - const x = state.pointer.active - ? state.pointer.x - : state.width * 0.56; - - const y = state.pointer.active - ? state.pointer.y - : state.height * 0.44; - - context.save(); - - context.strokeStyle = palette.planeMuted; - context.lineWidth = 1; - + function drawRadar(p) { + const x = state.pointer.active ? state.pointer.x : state.width * 0.56; + const y = state.pointer.active ? state.pointer.y : state.height * 0.44; + ctx.save(); + ctx.strokeStyle = p.planeMuted; + ctx.lineWidth = 1; for (const radius of [24, 48, 82]) { - context.globalAlpha = 0.24 - radius / 500; - - context.beginPath(); - context.arc(x, y, radius, 0, Math.PI * 2); - context.stroke(); + ctx.globalAlpha = 0.24 - radius / 500; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.stroke(); } - - context.globalAlpha = 0.2; - - context.beginPath(); - context.moveTo(x - 100, y); - context.lineTo(x + 100, y); - context.moveTo(x, y - 100); - context.lineTo(x, y + 100); - context.stroke(); - - context.restore(); + ctx.globalAlpha = 0.2; + ctx.beginPath(); + ctx.moveTo(x - 100, y); + ctx.lineTo(x + 100, y); + ctx.moveTo(x, y - 100); + ctx.lineTo(x, y + 100); + ctx.stroke(); + ctx.restore(); } - function drawYagi(station, palette) { - context.save(); + function drawYagi(station, p) { + ctx.save(); + ctx.translate(station.x, station.y); + ctx.rotate(station.angle); + ctx.strokeStyle = p.plane; + ctx.fillStyle = p.plane; + ctx.lineWidth = 1.5; + ctx.lineCap = "round"; + ctx.shadowColor = p.beamGlow; + ctx.shadowBlur = 9; - context.translate(station.x, station.y); - context.rotate(station.angle); + ctx.beginPath(); + ctx.moveTo(-30, 26); + ctx.lineTo(-30, -5); + ctx.stroke(); - context.strokeStyle = palette.plane; - context.fillStyle = palette.plane; - context.lineCap = "round"; - context.lineWidth = 1.5; + ctx.beginPath(); + ctx.moveTo(-32, 0); + ctx.lineTo(43, 0); + ctx.stroke(); - context.shadowColor = palette.beamGlow; - context.shadowBlur = 9; - - /* - * Mast - */ - context.beginPath(); - context.moveTo(-30, 26); - context.lineTo(-30, -5); - context.stroke(); - - /* - * Ausleger / Boom - */ - context.beginPath(); - context.moveTo(-32, 0); - context.lineTo(43, 0); - context.stroke(); - - /* - * Reflektor - */ - context.beginPath(); - context.moveTo(-27, -17); - context.lineTo(-27, 17); - context.stroke(); - - /* - * Dipol - */ - context.beginPath(); - context.moveTo(-12, -14); - context.lineTo(-12, 14); - context.stroke(); - - /* - * Direktoren - */ - const directors = [ - { x: 2, length: 22 }, - { x: 14, length: 19 }, - { x: 25, length: 16 }, - { x: 35, length: 13 } - ]; - - for (const director of directors) { - context.beginPath(); - context.moveTo( - director.x, - -director.length / 2 - ); - context.lineTo( - director.x, - director.length / 2 - ); - context.stroke(); - } - - /* - * Einspeisepunkt - */ - context.beginPath(); - context.arc(-12, 0, 3.2, 0, Math.PI * 2); - context.fill(); - - context.restore(); - - /* - * Locator bleibt waagerecht lesbar. - */ - context.save(); - - context.font = - '700 11px Inter, system-ui, sans-serif'; - - const labelWidth = - context.measureText(station.label).width + 14; - - const labelX = station.x - labelWidth / 2; - const labelY = station.y + 32; - - context.fillStyle = "rgba(6, 18, 10, 0.86)"; - context.strokeStyle = palette.planeMuted; - context.lineWidth = 1; - - context.beginPath(); - context.roundRect( - labelX, - labelY, - labelWidth, - 23, - 6 - ); - context.fill(); - context.stroke(); - - context.fillStyle = palette.plane; - context.textAlign = "center"; - context.textBaseline = "middle"; - - context.fillText( - station.label, - station.x, - labelY + 11.5 - ); - - context.restore(); + for (const element of [ + { x: -27, length: 34 }, + { x: -12, length: 28 }, + { x: 2, length: 22 }, + { x: 14, length: 19 }, + { x: 25, length: 16 }, + { x: 35, length: 13 } + ]) { + ctx.beginPath(); + ctx.moveTo(element.x, -element.length / 2); + ctx.lineTo(element.x, element.length / 2); + ctx.stroke(); } - function drawPlane(plane, palette) { - context.save(); + ctx.beginPath(); + ctx.arc(-12, 0, 3.2, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); - context.translate(plane.x, plane.y); - context.rotate(plane.angle); - - const alpha = clamp( - Math.min( - plane.age * 1.8, - (plane.maximumAge - plane.age) * 1.8 - ), - 0, - 1 - ); - - context.globalAlpha = alpha; - - context.strokeStyle = palette.trail; - context.lineWidth = 1; - - context.beginPath(); - context.moveTo(-plane.size * 4.5, 0); - context.lineTo(-plane.size * 1.4, 0); - context.stroke(); - - context.fillStyle = - plane.hit > 0 - ? palette.reflection - : palette.plane; - - context.shadowColor = - plane.hit > 0 - ? palette.reflection - : palette.beamGlow; - - context.shadowBlur = - plane.hit > 0 - ? 16 - : 5; - - const size = plane.size; - - context.beginPath(); - - context.moveTo(size * 1.65, 0); - context.lineTo(-size * 0.7, size * 0.34); - context.lineTo(-size * 0.15, size * 1.05); - context.lineTo(-size * 0.65, size * 1.08); - context.lineTo(-size * 1.35, size * 0.3); - context.lineTo(-size * 1.7, size * 0.28); - context.lineTo(-size * 1.12, 0); - context.lineTo(-size * 1.7, -size * 0.28); - context.lineTo(-size * 1.35, -size * 0.3); - context.lineTo(-size * 0.65, -size * 1.08); - context.lineTo(-size * 0.15, -size * 1.05); - context.lineTo(-size * 0.7, -size * 0.34); - - context.closePath(); - context.fill(); - - context.restore(); + ctx.save(); + ctx.font = "700 11px Inter, system-ui, sans-serif"; + const width = ctx.measureText(station.label).width + 14; + const x = station.x - width / 2; + const y = station.y + 32; + ctx.fillStyle = "rgba(6,18,10,.86)"; + ctx.strokeStyle = p.planeMuted; + roundedRect(x, y, width, 23, 6); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = p.plane; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(station.label, station.x, y + 11.5); + ctx.restore(); } - function drawBeam(beam, palette) { - const progress = beam.age / beam.maximumAge; + function drawPlane(plane, p) { + ctx.save(); + ctx.translate(plane.x, plane.y); + ctx.rotate(plane.angle); + ctx.globalAlpha = clamp(Math.min(plane.age * 1.8, (plane.maxAge - plane.age) * 1.8), 0, 1); + ctx.strokeStyle = p.trail; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(-plane.size * 4.5, 0); + ctx.lineTo(-plane.size * 1.4, 0); + ctx.stroke(); + + ctx.fillStyle = plane.hit > 0 ? p.reflection : p.plane; + ctx.shadowColor = plane.hit > 0 ? p.reflection : p.beamGlow; + ctx.shadowBlur = plane.hit > 0 ? 16 : 5; + const s = plane.size; + ctx.beginPath(); + ctx.moveTo(s * 1.65, 0); + ctx.lineTo(-s * 0.7, s * 0.34); + ctx.lineTo(-s * 0.15, s * 1.05); + ctx.lineTo(-s * 0.65, s * 1.08); + ctx.lineTo(-s * 1.35, s * 0.3); + ctx.lineTo(-s * 1.7, s * 0.28); + ctx.lineTo(-s * 1.12, 0); + ctx.lineTo(-s * 1.7, -s * 0.28); + ctx.lineTo(-s * 1.35, -s * 0.3); + ctx.lineTo(-s * 0.65, -s * 1.08); + ctx.lineTo(-s * 0.15, -s * 1.05); + ctx.lineTo(-s * 0.7, -s * 0.34); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + + function drawBeam(beam, p) { + const progress = beam.age / beam.maxAge; const alpha = Math.sin(progress * Math.PI); - - context.save(); - - context.globalAlpha = alpha; - context.strokeStyle = palette.beamGlow; - context.lineWidth = 8; - context.shadowColor = palette.beam; - context.shadowBlur = 15; - - context.setLineDash([10, 6]); - context.lineDashOffset = -progress * 36; - - context.beginPath(); - context.moveTo(beam.startX, beam.startY); - context.lineTo(beam.endX, beam.endY); - context.stroke(); - - context.strokeStyle = palette.beam; - context.lineWidth = 1.8; - context.shadowBlur = 5; - - context.beginPath(); - context.moveTo(beam.startX, beam.startY); - context.lineTo(beam.endX, beam.endY); - context.stroke(); - - context.setLineDash([]); - - context.restore(); + ctx.save(); + ctx.globalAlpha = alpha; + ctx.strokeStyle = p.beamGlow; + ctx.lineWidth = 8; + ctx.shadowColor = p.beam; + ctx.shadowBlur = 15; + ctx.setLineDash([10, 6]); + ctx.lineDashOffset = -progress * 36; + ctx.beginPath(); + ctx.moveTo(beam.startX, beam.startY); + ctx.lineTo(beam.endX, beam.endY); + ctx.stroke(); + ctx.setLineDash([]); + ctx.strokeStyle = p.beam; + ctx.lineWidth = 1.8; + ctx.shadowBlur = 5; + ctx.beginPath(); + ctx.moveTo(beam.startX, beam.startY); + ctx.lineTo(beam.endX, beam.endY); + ctx.stroke(); + ctx.restore(); } - function drawReflection(reflection, palette) { - const progress = - reflection.age / reflection.maximumAge; - - const alpha = 1 - progress; - - context.save(); - - context.translate( - reflection.x, - reflection.y - ); - - context.globalAlpha = alpha; - - /* - * Äußerer Reflexionsring - */ - context.strokeStyle = palette.reflection; - context.lineWidth = 1.6; - context.shadowColor = palette.beam; - context.shadowBlur = 18; - - context.beginPath(); - context.arc( - 0, - 0, - reflection.radius, - 0, - Math.PI * 2 - ); - context.stroke(); - - /* - * Zweiter Ring - */ - context.globalAlpha = alpha * 0.55; - - context.beginPath(); - context.arc( - 0, - 0, - reflection.radius * 1.65, - 0, - Math.PI * 2 - ); - context.stroke(); - - /* - * Reflexionskreuz - */ - context.globalAlpha = alpha * 0.8; - context.lineWidth = 1; - - const crossSize = - reflection.radius * 1.25; - - context.beginPath(); - context.moveTo(-crossSize, 0); - context.lineTo(crossSize, 0); - context.moveTo(0, -crossSize); - context.lineTo(0, crossSize); - context.stroke(); - - /* - * Heller Reflexionspunkt - */ - context.globalAlpha = alpha; - context.fillStyle = palette.reflection; - context.shadowBlur = 24; - - context.beginPath(); - context.arc(0, 0, 3.8, 0, Math.PI * 2); - context.fill(); - - context.restore(); - - /* - * AP-Prozentwert - */ - context.save(); - - const label = `${reflection.probability}%`; - - context.font = - '800 12px Inter, system-ui, sans-serif'; - - const textWidth = - context.measureText(label).width; - - const boxWidth = textWidth + 16; - const boxHeight = 25; - - const boxX = - reflection.x + reflection.radius + 12; - - const boxY = - reflection.y - reflection.radius - 17; - - context.globalAlpha = - clamp(alpha * 1.25, 0, 1); - - context.fillStyle = "rgba(6, 18, 10, 0.91)"; - context.strokeStyle = palette.plane; - context.lineWidth = 1; - - context.shadowColor = palette.beamGlow; - context.shadowBlur = 10; - - context.beginPath(); - context.roundRect( - boxX, - boxY, - boxWidth, - boxHeight, - 6 - ); - context.fill(); - context.stroke(); - - context.shadowBlur = 0; - context.fillStyle = palette.reflection; - context.textAlign = "center"; - context.textBaseline = "middle"; - - context.fillText( - label, - boxX + boxWidth / 2, - boxY + boxHeight / 2 - ); - - context.restore(); + function drawReflection(r, p) { + const progress = r.age / r.maxAge; + const alpha = 1 - progress; + ctx.save(); + ctx.translate(r.x, r.y); + ctx.globalAlpha = alpha; + ctx.strokeStyle = p.reflection; + ctx.lineWidth = 1.6; + ctx.shadowColor = p.beam; + ctx.shadowBlur = 18; + for (const scale of [1, 1.65]) { + ctx.globalAlpha = alpha * (scale === 1 ? 1 : 0.55); + ctx.beginPath(); + ctx.arc(0, 0, r.radius * scale, 0, Math.PI * 2); + ctx.stroke(); } + ctx.globalAlpha = alpha * 0.8; + const cross = r.radius * 1.25; + ctx.beginPath(); + ctx.moveTo(-cross, 0); + ctx.lineTo(cross, 0); + ctx.moveTo(0, -cross); + ctx.lineTo(0, cross); + ctx.stroke(); + ctx.globalAlpha = alpha; + ctx.fillStyle = p.reflection; + ctx.beginPath(); + ctx.arc(0, 0, 3.8, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); - function drawStaticScene() { - const palette = getPalette(); - - context.clearRect( - 0, - 0, - state.width, - state.height - ); - - drawGrid(palette); - drawRadarTarget(palette); - - for (const station of getRadioStations()) { - drawYagi(station, palette); - } + ctx.save(); + const label = `${r.probability}%`; + ctx.font = "800 12px Inter, system-ui, sans-serif"; + const width = ctx.measureText(label).width + 16; + const height = 25; + const x = r.x + r.radius + 12; + const y = r.y - r.radius - 17; + ctx.globalAlpha = clamp(alpha * 1.25, 0, 1); + ctx.fillStyle = "rgba(6,18,10,.91)"; + ctx.strokeStyle = p.plane; + ctx.shadowColor = p.beamGlow; + ctx.shadowBlur = 10; + roundedRect(x, y, width, height, 6); + ctx.fill(); + ctx.stroke(); + ctx.shadowBlur = 0; + ctx.fillStyle = p.reflection; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(label, x + width / 2, y + height / 2); + ctx.restore(); } - function drawScene() { - const palette = getPalette(); - - context.clearRect( - 0, - 0, - state.width, - state.height - ); - - drawGrid(palette); - drawRadarTarget(palette); - - for (const station of getRadioStations()) { - drawYagi(station, palette); - } - - for (const plane of state.planes) { - drawPlane(plane, palette); - } - - for (const beam of state.beams) { - drawBeam(beam, palette); - } - - for (const reflection of state.reflections) { - drawReflection(reflection, palette); - } + function draw(staticOnly = false) { + const p = palette(); + ctx.clearRect(0, 0, state.width, state.height); + drawGrid(p); + drawRadar(p); + stations().forEach(station => drawYagi(station, p)); + if (staticOnly) return; + state.planes.forEach(plane => drawPlane(plane, p)); + state.beams.forEach(beam => drawBeam(beam, p)); + state.reflections.forEach(r => drawReflection(r, p)); } function animate(timestamp) { - if (!state.running) { - return; - } - - const deltaSeconds = clamp( - (timestamp - state.lastFrame) / 1000 || 0, - 0, - 0.05 - ); - + if (!state.running) return; + const dt = clamp((timestamp - state.lastFrame) / 1000 || 0, 0, 0.05); state.lastFrame = timestamp; if (!state.visible || document.hidden) { - state.animationFrameId = - requestAnimationFrame(animate); - + state.frameId = requestAnimationFrame(animate); return; } - state.spawnAccumulator += deltaSeconds; - state.beamAccumulator += deltaSeconds; + state.spawnTime += dt; + state.beamTime += dt; + const spawnInterval = coarsePointer.matches ? 2.2 : 1.15; + const beamInterval = coarsePointer.matches ? 3.3 : 1.8; - const planeLimit = getPlaneLimit(); - const spawnInterval = - coarsePointer.matches ? 2.2 : 1.15; - - if ( - state.spawnAccumulator >= spawnInterval && - state.planes.length < planeLimit - ) { - state.spawnAccumulator = 0; + if (state.spawnTime >= spawnInterval && state.planes.length < planeLimit()) { + state.spawnTime = 0; state.planes.push(createPlane()); } - const beamInterval = - coarsePointer.matches ? 3.3 : 1.8; - - if ( - state.beamAccumulator >= beamInterval && - state.planes.length > 0 - ) { - state.beamAccumulator = 0; - - const target = chooseBeamTarget(); - - if (target) { - createBeam(target); - } + if (state.beamTime >= beamInterval && state.planes.length) { + state.beamTime = 0; + const target = beamTarget(); + if (target) createBeam(target); } - for (const plane of state.planes) { - updatePlane(plane, deltaSeconds); - } - - state.planes = state.planes.filter( - (plane) => !isPlaneExpired(plane) - ); - - updateEffects(deltaSeconds); - drawScene(); - - state.animationFrameId = - requestAnimationFrame(animate); + state.planes.forEach(plane => updatePlane(plane, dt)); + state.planes = state.planes.filter(plane => !expired(plane)); + updateEffects(dt); + draw(false); + state.frameId = requestAnimationFrame(animate); } function start() { - cancelAnimationFrame(state.animationFrameId); - - resizeCanvas(); - + cancelAnimationFrame(state.frameId); + resize(); if (reducedMotion.matches) { state.running = false; - drawStaticScene(); + draw(true); return; } state.running = true; state.lastFrame = performance.now(); - - if (state.planes.length === 0) { - const initialPlanes = - coarsePointer.matches ? 2 : 5; - - for (let index = 0; index < initialPlanes; index++) { + if (!state.planes.length) { + const count = coarsePointer.matches ? 2 : 5; + for (let i = 0; i < count; i++) { const plane = createPlane(); plane.age = Math.random() * 2; state.planes.push(plane); } } - - state.animationFrameId = - requestAnimationFrame(animate); + state.frameId = requestAnimationFrame(animate); } - function updatePointer(event) { - const bounds = hero.getBoundingClientRect(); - - state.pointer.x = clamp( - event.clientX - bounds.left, - 0, - bounds.width - ); - - state.pointer.y = clamp( - event.clientY - bounds.top, - 0, - bounds.height - ); - + hero.addEventListener("pointermove", event => { + const rect = hero.getBoundingClientRect(); + state.pointer.x = clamp(event.clientX - rect.left, 0, rect.width); + state.pointer.y = clamp(event.clientY - rect.top, 0, rect.height); state.pointer.active = true; - } - - hero.addEventListener("pointermove", updatePointer, { - passive: true - }); - - hero.addEventListener("pointerleave", () => { - state.pointer.active = false; - }); - - window.addEventListener("resize", resizeCanvas, { - passive: true - }); - - document.addEventListener("visibilitychange", () => { - state.lastFrame = performance.now(); - }); + }, { passive: true }); + hero.addEventListener("pointerleave", () => state.pointer.active = false); + addEventListener("resize", resize, { passive: true }); + document.addEventListener("visibilitychange", () => state.lastFrame = performance.now()); reducedMotion.addEventListener("change", start); - const resizeObserver = new ResizeObserver(() => { - resizeCanvas(); + new ResizeObserver(() => { + resize(); + if (reducedMotion.matches) draw(true); + }).observe(hero); - if (reducedMotion.matches) { - drawStaticScene(); - } - }); - - resizeObserver.observe(hero); - - const intersectionObserver = new IntersectionObserver( - (entries) => { - state.visible = - entries[0]?.isIntersecting ?? true; - - state.lastFrame = performance.now(); - }, - { - threshold: 0.05 - } - ); - - intersectionObserver.observe(hero); + new IntersectionObserver(entries => { + state.visible = entries[0]?.isIntersecting ?? true; + state.lastFrame = performance.now(); + }, { threshold: 0.05 }).observe(hero); start(); -})(); \ No newline at end of file +})(); diff --git a/website/_site/contact/index.html b/website/_site/contact/index.html index e07d3d4..de2733f 100644 --- a/website/_site/contact/index.html +++ b/website/_site/contact/index.html @@ -4,6 +4,7 @@ Contact | KST4Contest + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Contact

Contact

@@ -91,8 +90,7 @@ Join the mailing list → -
-
+ - - \ No newline at end of file + diff --git a/website/_site/download/index.html b/website/_site/download/index.html index 2e11ce1..44a6e60 100644 --- a/website/_site/download/index.html +++ b/website/_site/download/index.html @@ -4,6 +4,7 @@ Download KST4Contest + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Download

Get KST4Contest

@@ -255,8 +254,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/faq/index.html b/website/_site/faq/index.html index a8ca646..9ae1da5 100644 --- a/website/_site/faq/index.html +++ b/website/_site/faq/index.html @@ -4,6 +4,7 @@ KST4Contest FAQ + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Frequently asked questions

@@ -134,8 +133,7 @@ } ] } - -

+ - - \ No newline at end of file + diff --git a/website/_site/features/airscout/index.html b/website/_site/features/airscout/index.html index e2f5df1..406374a 100644 --- a/website/_site/features/airscout/index.html +++ b/website/_site/features/airscout/index.html @@ -4,6 +4,7 @@ AirScout Integration + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Aircraft Scatter · since 1.26

AirScout Integration

@@ -189,8 +188,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/dual-chat/index.html b/website/_site/features/dual-chat/index.html index 0fa3ea8..7611711 100644 --- a/website/_site/features/dual-chat/index.html +++ b/website/_site/features/dual-chat/index.html @@ -4,6 +4,7 @@ Dual Chat Categories + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

ON4KST Chat · since 1.26

Dual Chat Categories

@@ -165,8 +164,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/dx-cluster/index.html b/website/_site/features/dx-cluster/index.html index d3890e5..5f0777f 100644 --- a/website/_site/features/dx-cluster/index.html +++ b/website/_site/features/dx-cluster/index.html @@ -4,6 +4,7 @@ DXCluster Server + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Radio Workflow · since 1.23

DXCluster Server

@@ -164,8 +163,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/index.html b/website/_site/features/index.html index febe429..b128a46 100644 --- a/website/_site/features/index.html +++ b/website/_site/features/index.html @@ -4,6 +4,7 @@ KST4Contest Features + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Features

Contest tools, not just chat windows.

@@ -150,8 +149,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/features/log-sync/index.html b/website/_site/features/log-sync/index.html index 75851c5..a1a0d14 100644 --- a/website/_site/features/log-sync/index.html +++ b/website/_site/features/log-sync/index.html @@ -4,6 +4,7 @@ Log Synchronization + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Logger Integration · since 1.31

Log Synchronization

@@ -165,8 +164,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/macros/index.html b/website/_site/features/macros/index.html index 939281d..cd3a70f 100644 --- a/website/_site/features/macros/index.html +++ b/website/_site/features/macros/index.html @@ -4,6 +4,7 @@ Macros and Variables + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Operator Speed · since 1.0

Macros and Variables

@@ -164,8 +163,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/priority-score/index.html b/website/_site/features/priority-score/index.html index e7cbe5a..541eb2c 100644 --- a/website/_site/features/priority-score/index.html +++ b/website/_site/features/priority-score/index.html @@ -4,6 +4,7 @@ Priority Score System + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Contest Workflow · since 1.40

Priority Score System

@@ -193,8 +192,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/sked-reminder/index.html b/website/_site/features/sked-reminder/index.html index 51a7bde..34a2922 100644 --- a/website/_site/features/sked-reminder/index.html +++ b/website/_site/features/sked-reminder/index.html @@ -4,6 +4,7 @@ Sked Reminder + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Sked Management · since 1.40

Sked Reminder

@@ -187,8 +186,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/features/timeline/index.html b/website/_site/features/timeline/index.html index 0e09da8..2f83f7d 100644 --- a/website/_site/features/timeline/index.html +++ b/website/_site/features/timeline/index.html @@ -4,6 +4,7 @@ Timeline View + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Contest Awareness · since 1.40

Timeline View

@@ -165,8 +164,7 @@ "name": "KST4Contest" } } - -
+ - - \ No newline at end of file + diff --git a/website/_site/index.html b/website/_site/index.html index d355c52..4475301 100644 --- a/website/_site/index.html +++ b/website/_site/index.html @@ -4,6 +4,7 @@ KST4Contest – Contest-Optimized ON4KST Chat Client + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-
- - - + -
+ +

The contest-optimized ON4KST client

KST4Contest

@@ -247,7 +241,7 @@

@@ -264,7 +258,7 @@ "description":"Contest-optimized ON4KST client for VHF, UHF and SHF amateur radio contests." } -
+ - + - - \ No newline at end of file + diff --git a/website/_site/legal-notice/index.html b/website/_site/legal-notice/index.html index 4be4244..df241c5 100644 --- a/website/_site/legal-notice/index.html +++ b/website/_site/legal-notice/index.html @@ -4,6 +4,7 @@ Legal Notice | KST4Contest + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Legal Notice

Legal Notice

@@ -101,8 +100,7 @@ GitHub.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/airscout-integration/index.html b/website/_site/manual/de/airscout-integration/index.html index 94bbd4e..ea9f042 100644 --- a/website/_site/manual/de/airscout-integration/index.html +++ b/website/_site/manual/de/airscout-integration/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -175,8 +174,7 @@

In der Benutzerliste gibt es einen Button mit einem Pfeil, der die Richtung (QTF) zur ausgewählten Station anzeigt. Ein Klick maximiert AirScout und zeigt den Pfad mit reflektierbaren Flugzeugen zum ausgewählten Gesprächspartner.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/benutzeroberflaeche/index.html b/website/_site/manual/de/benutzeroberflaeche/index.html index fe1b806..37d4e0b 100644 --- a/website/_site/manual/de/benutzeroberflaeche/index.html +++ b/website/_site/manual/de/benutzeroberflaeche/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -177,8 +176,7 @@
-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/changelog/index.html b/website/_site/manual/de/changelog/index.html index d558fbf..c8c1c71 100644 --- a/website/_site/manual/de/changelog/index.html +++ b/website/_site/manual/de/changelog/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -312,8 +311,7 @@
-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/dx-cluster-server/index.html b/website/_site/manual/de/dx-cluster-server/index.html index 764c6ea..238b5b9 100644 --- a/website/_site/manual/de/dx-cluster-server/index.html +++ b/website/_site/manual/de/dx-cluster-server/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -139,8 +138,7 @@

Weitere Testergebnisse sind willkommen – bitte per E-Mail an DO5AMF melden.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/funktionen/index.html b/website/_site/manual/de/funktionen/index.html index 5fc7860..ef4fdae 100644 --- a/website/_site/manual/de/funktionen/index.html +++ b/website/_site/manual/de/funktionen/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -261,8 +260,7 @@

Damit werden unbrauchbare, abgeschnittene Fenster beim Wechsel zwischen Geräten oder Monitoren verhindert.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/home/index.html b/website/_site/manual/de/home/index.html index 35ef7c5..b10a57b 100644 --- a/website/_site/manual/de/home/index.html +++ b/website/_site/manual/de/home/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -168,8 +167,7 @@

Besonderer Dank gilt: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX-Cluster-Idee), DC9DJ (Konrad Neitzel, Projektstruktur), DO5ALF (Andreas, Webmaster funkerportal.de), PE0WGA (Franz van Velzen, Tester) sowie allen weiteren Testern und Ideengebern.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/index.html b/website/_site/manual/de/index.html index 7c24696..db1a67b 100644 --- a/website/_site/manual/de/index.html +++ b/website/_site/manual/de/index.html @@ -4,6 +4,7 @@ KST4Contest Deutsches Handbuch + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Deutsches Handbuch

Das Handbuch wird zentral in GitHub gepflegt und als Wiki, PDF und Online-Dokumentation veröffentlicht.

@@ -164,8 +163,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/installation/index.html b/website/_site/manual/de/installation/index.html index 7bdcb6e..742679d 100644 --- a/website/_site/manual/de/installation/index.html +++ b/website/_site/manual/de/installation/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -283,8 +282,7 @@ flatpak install kst4contest de.x08.KST4Contest//nightly

(Gemeldet von PE0WGA, Franz van Velzen – danke!)

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/konfiguration/index.html b/website/_site/manual/de/konfiguration/index.html index 6d3b321..e26c584 100644 --- a/website/_site/manual/de/konfiguration/index.html +++ b/website/_site/manual/de/konfiguration/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -219,8 +218,7 @@ Zudem wird die primäre Chat-Kategorie (z. B. IARU Region 1 VHF
-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/log-synchronisation/index.html b/website/_site/manual/de/log-synchronisation/index.html index 136babf..b587d2e 100644 --- a/website/_site/manual/de/log-synchronisation/index.html +++ b/website/_site/manual/de/log-synchronisation/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -197,8 +196,7 @@

Vor jedem neuen Contest: Datenbank zurücksetzen! → Konfiguration – Worked Station Database Settings

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/de/makros-und-variablen/index.html b/website/_site/manual/de/makros-und-variablen/index.html index 4677286..6996c5f 100644 --- a/website/_site/manual/de/makros-und-variablen/index.html +++ b/website/_site/manual/de/makros-und-variablen/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -208,8 +207,7 @@

Ohne manuelle Tipparbeit, ohne Fehler, ohne Unterbrechung des CQ-Rufens.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/airscout-integration/index.html b/website/_site/manual/en/airscout-integration/index.html index d939126..2564b34 100644 --- a/website/_site/manual/en/airscout-integration/index.html +++ b/website/_site/manual/en/airscout-integration/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -175,8 +174,7 @@

In the user list there is a button with an arrow showing the direction (QTF) to the selected station. Clicking it maximises AirScout and shows the path with reflectable aircraft to the selected contact.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/changelog/index.html b/website/_site/manual/en/changelog/index.html index c519587..56e05ae 100644 --- a/website/_site/manual/en/changelog/index.html +++ b/website/_site/manual/en/changelog/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -312,8 +311,7 @@
-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/configuration/index.html b/website/_site/manual/en/configuration/index.html index 3934f50..f34aae1 100644 --- a/website/_site/manual/en/configuration/index.html +++ b/website/_site/manual/en/configuration/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -219,8 +218,7 @@ Also, select the primary chat category (e.g., IARU Region 1 VHF
-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/dx-cluster-server/index.html b/website/_site/manual/en/dx-cluster-server/index.html index 1c949b3..a25a443 100644 --- a/website/_site/manual/en/dx-cluster-server/index.html +++ b/website/_site/manual/en/dx-cluster-server/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -139,8 +138,7 @@

Further test reports are welcome – please send by email to DO5AMF.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/features/index.html b/website/_site/manual/en/features/index.html index 425b9c9..f1bc793 100644 --- a/website/_site/manual/en/features/index.html +++ b/website/_site/manual/en/features/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -260,8 +259,7 @@

This prevents unusable oversized windows when switching between machines or monitors.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/home/index.html b/website/_site/manual/en/home/index.html index ca26bb5..32153c9 100644 --- a/website/_site/manual/en/home/index.html +++ b/website/_site/manual/en/home/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -168,8 +167,7 @@

Special thanks to: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX cluster idea), DC9DJ (Konrad Neitzel, project structure), DO5ALF (Andreas, webmaster funkerportal.de), PE0WGA (Franz van Velzen, tester) and all other testers and contributors.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/index.html b/website/_site/manual/en/index.html index df7b65e..1c2b5da 100644 --- a/website/_site/manual/en/index.html +++ b/website/_site/manual/en/index.html @@ -4,6 +4,7 @@ KST4Contest English Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

English Manual

The manual is maintained centrally in GitHub and published as Wiki, PDF and online documentation.

@@ -164,8 +163,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/installation/index.html b/website/_site/manual/en/installation/index.html index 3a25cb3..de07ac1 100644 --- a/website/_site/manual/en/installation/index.html +++ b/website/_site/manual/en/installation/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -283,8 +282,7 @@ flatpak install kst4contest de.x08.KST4Contest//nightly

(Reported by PE0WGA, Franz van Velzen – thank you!)

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/log-sync/index.html b/website/_site/manual/en/log-sync/index.html index e390c7e..15d089f 100644 --- a/website/_site/manual/en/log-sync/index.html +++ b/website/_site/manual/en/log-sync/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -197,8 +196,7 @@

Before each new contest: reset the database! → Configuration – Worked Station Database Settings

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/macros-and-variables/index.html b/website/_site/manual/en/macros-and-variables/index.html index 9212d33..7f9f0cd 100644 --- a/website/_site/manual/en/macros-and-variables/index.html +++ b/website/_site/manual/en/macros-and-variables/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -208,8 +207,7 @@

No manual typing, no errors, no interruption to CQ calling.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/en/user-interface/index.html b/website/_site/manual/en/user-interface/index.html index 11ef735..ff47dc7 100644 --- a/website/_site/manual/en/user-interface/index.html +++ b/website/_site/manual/en/user-interface/index.html @@ -4,6 +4,7 @@ {{ manual.title }} | KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

← Back to manual overview

@@ -177,8 +176,7 @@
-
-
+ - - \ No newline at end of file + diff --git a/website/_site/manual/index.html b/website/_site/manual/index.html index 67c618c..0d7dff9 100644 --- a/website/_site/manual/index.html +++ b/website/_site/manual/index.html @@ -4,6 +4,7 @@ KST4Contest Manual + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

KST4Contest Manual

@@ -76,8 +75,7 @@ English Manual Deutsches Handbuch -

-
+ - - \ No newline at end of file + diff --git a/website/_site/news/2026-07-08-hotfix-version-1-41-1/index.html b/website/_site/news/2026-07-08-hotfix-version-1-41-1/index.html index a612da2..0c963bc 100644 --- a/website/_site/news/2026-07-08-hotfix-version-1-41-1/index.html +++ b/website/_site/news/2026-07-08-hotfix-version-1-41-1/index.html @@ -4,6 +4,7 @@ Hotfix Version 1.41.1 + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

News · July 8, 2026

Hotfix Version 1.41.1

@@ -77,8 +76,7 @@

We had a bug, where KST4Contest would clear out the text field, where you would send your messages after a period. It also happend to set your Cursor back to the Sending Field, when you were filtering for a station. We fixed this bug today. The new release is here. You can also download the artifacts as always via the Downloads-Page.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/news/2026-07-website-launch/index.html b/website/_site/news/2026-07-website-launch/index.html index f8d104d..3742069 100644 --- a/website/_site/news/2026-07-website-launch/index.html +++ b/website/_site/news/2026-07-website-launch/index.html @@ -4,6 +4,7 @@ KST4Contest Website Launch + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

News · July 7, 2026

KST4Contest Website Launch

@@ -81,8 +80,7 @@

Documentation, release information, feature pages and future updates can now grow from a shared content model.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/news/index.html b/website/_site/news/index.html index d954a61..5cfc1ff 100644 --- a/website/_site/news/index.html +++ b/website/_site/news/index.html @@ -4,6 +4,7 @@ KST4Contest News + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

News

Development updates and release notes.

@@ -91,8 +90,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/privacy/index.html b/website/_site/privacy/index.html index d5b0544..823a066 100644 --- a/website/_site/privacy/index.html +++ b/website/_site/privacy/index.html @@ -4,6 +4,7 @@ Privacy Policy | KST4Contest + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Privacy

Privacy Policy

@@ -132,8 +131,7 @@ You have the right to lodge a complaint with a competent data protection supervisory authority.

-
-
+ - - \ No newline at end of file + diff --git a/website/_site/roadmap/index.html b/website/_site/roadmap/index.html index 3f5617f..3babff9 100644 --- a/website/_site/roadmap/index.html +++ b/website/_site/roadmap/index.html @@ -4,6 +4,7 @@ KST4Contest Roadmap + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Roadmap

What's planned next.

@@ -216,8 +215,7 @@ href="/support/"> -
-
+ - - \ No newline at end of file + diff --git a/website/_site/screenshots/index.html b/website/_site/screenshots/index.html index 268dfaf..d37a6de 100644 --- a/website/_site/screenshots/index.html +++ b/website/_site/screenshots/index.html @@ -4,6 +4,7 @@ KST4Contest Screenshots + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Screenshots

See the contest workflow.

@@ -102,8 +101,7 @@ -
-
+ - - \ No newline at end of file + diff --git a/website/_site/support/index.html b/website/_site/support/index.html index 834ebf8..5c3280c 100644 --- a/website/_site/support/index.html +++ b/website/_site/support/index.html @@ -4,6 +4,7 @@ Support KST4Contest + @@ -12,34 +13,33 @@ + + + + + - + - - - - - - -
+
-

Support Development

@@ -234,8 +233,7 @@ rel="noopener"> -
-
+ - - \ No newline at end of file +