diff --git a/docs/concepts/earthquake-early-warning.md b/docs/concepts/earthquake-early-warning.md
index 89d93f3d..fc1d7156 100644
--- a/docs/concepts/earthquake-early-warning.md
+++ b/docs/concepts/earthquake-early-warning.md
@@ -2,6 +2,8 @@
title: Earthquake Early Warning
---
+import {WarningTimeDiagram} from '@site/src/components/Diagrams';
+
# Earthquake Early Warning
Earthquake early warning (EEW) detects an earthquake after it begins and attempts to notify locations before damaging shaking reaches them. It is not earthquake prediction.
@@ -30,6 +32,8 @@ Every step consumes time. Warning time depends on the earthquake, station geomet
Locations close to the source can experience strong shaking before the system has enough observations to issue a useful alert. No network density or communications technology eliminates this blind zone completely.
+
+
## Initial estimates change
The earliest data represent only the beginning of a rupture. Automated estimates can change as:
diff --git a/docs/concepts/iot-approach.md b/docs/concepts/iot-approach.md
index 217603d5..79b2c0ec 100644
--- a/docs/concepts/iot-approach.md
+++ b/docs/concepts/iot-approach.md
@@ -2,6 +2,8 @@
title: Connected Sensor Networks
---
+import {DataPathDiagram} from '@site/src/components/Diagrams';
+
# Connected Sensor Networks
Connected sensing combines compact instruments, standard communications, remote health telemetry, and centralized processing. It can make dense deployments practical, but connectivity does not replace good instrumentation or network design.
@@ -85,6 +87,8 @@ A connected station is an operational system. Plan for:
## Grillo architecture
+
+
- **Grillo Pulse and Grillo One** send health reports and seismic data in real time over Ethernet, Wi-Fi, or cellular, initiating every connection outbound.
- **Grillo Cloud** manages projects, claimed sensors, station metadata, device health, and over-the-air firmware updates. Its [SISTEM add-on](/events) adds waveforms and processes seismic data into earthquake events.
- **Seismic data** can go to Grillo or, per project, [to your own server](/dashboard/data/data-server), while health monitoring stays in Grillo Cloud.
diff --git a/docs/concepts/landslide-monitoring.md b/docs/concepts/landslide-monitoring.md
index d602a23d..5ac74c72 100644
--- a/docs/concepts/landslide-monitoring.md
+++ b/docs/concepts/landslide-monitoring.md
@@ -2,6 +2,8 @@
title: Landslide Monitoring with GNSS
---
+import {LandslideKitDiagram} from '@site/src/components/Diagrams';
+
# Landslide Monitoring with GNSS
Many landslides move slowly, by millimetres or centimetres over days to months, before they move fast. Measuring that slow movement shows which ground is active, how fast it is going, and whether it is speeding up.
@@ -14,6 +16,8 @@ A phone or a basic GPS receiver knows its position to within a few metres. Lands
**RTK** (real-time kinematic) positioning removes most of that error. A **base** receiver at a known, stable point watches the same satellites as a **rover** on the moving ground. Errors from the atmosphere and the satellites affect both receivers in almost the same way, so the base sends corrections and the rover cancels them out. Over short distances this gets the rover's position relative to the base down to centimetres or better.
+
+
When the rover has used the corrections to resolve its position precisely, it reports an **RTK Fixed** solution. Grillo Slide only uses RTK Fixed readings to measure movement.
## Movement is measured against the base
diff --git a/src/components/Diagrams/index.tsx b/src/components/Diagrams/index.tsx
new file mode 100644
index 00000000..73bd14a5
--- /dev/null
+++ b/src/components/Diagrams/index.tsx
@@ -0,0 +1,117 @@
+import type {ReactNode} from 'react';
+import styles from './styles.module.css';
+
+function Figure({title, caption, children}: {title: string; caption: string; children: ReactNode}): ReactNode {
+ return (
+
+
+ {caption}
+
+ );
+}
+
+function Device({x, y, label, sub, accent}: {x: number; y: number; label: string; sub?: string; accent?: boolean}): ReactNode {
+ return (
+
+
+ {label}
+ {sub && {sub}}
+
+ );
+}
+
+export function LandslideKitDiagram(): ReactNode {
+ return (
+
+
+
+ moving ground
+ stable ground
+
+
+ LTE
+
+
+
+
+
+
+
+
+
+
+ radio mesh
+
+ );
+}
+
+export function WarningTimeDiagram(): ReactNode {
+ return (
+
+
+ distance from epicentre
+
+ time after the earthquake starts
+
+
+ strong shaking arrives
+
+
+ alert sent
+
+
+ blind zone
+ shaking before the alert
+
+
+ warning time
+
+ );
+}
+
+function Box({x, y, w, label, sub, accent}: {x: number; y: number; w: number; label: string; sub: string; accent?: boolean}): ReactNode {
+ return (
+
+
+ {label}
+ {sub}
+
+ );
+}
+
+export function DataPathDiagram(): ReactNode {
+ return (
+
+
+
+
+ health · UDP 5683
+
+
+
+ seismic · UDP 5684 (default)
+
+
+
+ or, if Grillo sets it
+
+
+ );
+}
diff --git a/src/components/Diagrams/styles.module.css b/src/components/Diagrams/styles.module.css
new file mode 100644
index 00000000..7d6eac78
--- /dev/null
+++ b/src/components/Diagrams/styles.module.css
@@ -0,0 +1,98 @@
+.figure {
+ margin: 8px 0 28px;
+ padding: 16px;
+ border: 1px solid var(--border-hairline);
+ background: var(--bg-surface);
+}
+
+.svg {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.caption {
+ margin-top: 10px;
+ color: var(--fg-3);
+ font: 400 13px/1.5 var(--font-sans);
+}
+
+.label {
+ fill: var(--fg-1);
+ font: 500 13px var(--font-sans);
+}
+
+.sub {
+ fill: var(--fg-3);
+ font: 400 11px var(--font-mono);
+}
+
+.box {
+ fill: var(--bg-elevated);
+ stroke: var(--border-strong);
+ stroke-width: 1;
+}
+
+.boxAccent {
+ fill: var(--bg-elevated);
+ stroke: var(--brand-orange);
+ stroke-width: 1.5;
+}
+
+.line,
+.axis {
+ stroke: var(--fg-3);
+ stroke-width: 1.25;
+ fill: none;
+}
+
+.mesh {
+ stroke: var(--fg-3);
+ stroke-width: 1.25;
+ stroke-dasharray: 3 3;
+}
+
+.gap {
+ stroke: var(--fg-2);
+ stroke-width: 1.25;
+}
+
+.arrowHead {
+ fill: var(--fg-3);
+}
+
+.accentLine {
+ stroke: var(--brand-orange);
+ stroke-width: 1.5;
+ fill: none;
+}
+
+.accentFill {
+ fill: var(--brand-orange);
+}
+
+.accentText {
+ fill: var(--brand-orange);
+ font: 500 12px var(--font-mono);
+}
+
+.wave {
+ stroke: var(--fg-1);
+ stroke-width: 2;
+}
+
+.ground {
+ stroke: var(--fg-3);
+ stroke-width: 1.5;
+ fill: none;
+}
+
+.slope {
+ fill: var(--brand-orange);
+ opacity: 0.08;
+}
+
+.blind {
+ fill: var(--brand-orange);
+ opacity: 0.12;
+}