Skip to main content...

Contributing to ChroGPS Dash

First off, thank you for considering contributing to ChroGPS Dash! It’s people like you who make open-source tools better for the entire NTP/GNSS/precision time-keeping community.

Core Philosophy: “Zero Dependencies”

This project adheres to a strict no-dependency philosophy.

  • No external CSS frameworks (Bootstrap, Tailwind, etc.).
  • No external JS libraries (jQuery, Graph.js, Highgraphs, etc.).
  • No package managers (npm, composer, pip).
  • Single-file deployment: The entire dashboard logic resides in index.php.
  • Native, available APT packages: ChroGPS Dash is designed to use programs native to Debian (& Debian-like) systems for ease of deployment and for portability/repeatability.

How Can I Contribute?

Pull Requests

  1. Fork the repository and create your branch from master.
  2. Test locally: Ensure your changes work on a standard Debian/Ubuntu stack with chrony and gpsd.
  3. Responsive Check: Verify the UI layout on both Desktop (2-column grid) and Mobile (stacked single column).
  4. Theme Check: Toggle the Light/Dark mode to ensure all new elements use the correct CSS variables.
  5. Submit a Pull Request with a clear description of your changes.

Reporting Bugs & Feature Requests

  • Disabled and disregarded/dismissed: I only accept code contributions and bugfixes directly via Pull Requests.
  • Did I make it clear that only code contributions and bugfixes are accepted? 😉

Development Guidelines

1. PHP & Graphing (The “Native SVG” Engine)

I do not use client-side graphing libraries. All graphs are generated server-side using native PHP to output raw SVG XML.

  • Draw Function: Use the drawGraphSVG() helper function for all standard dual-axis line graphs. Its signature:
    drawGraphSVG($data, $labelY1, $labelY2, $unitY1, $unitY2, $colorY2 = 'var(--green)', $zeroBase = false, $staticLabel2 = null)
    
    $colorY2 sets the primary (left-axis) line color using a CSS variable token. Note: despite the Y2 suffix in the parameter name, this controls the primary/left-axis series color – not the secondary axis. This is a known naming inconsistency in the codebase. The secondary (right-axis) line always uses var(--accent). Pass $zeroBase = true to force the Y origin to zero (used for frequency graphs). Pass a string to $staticLabel2 to suppress the secondary line and use a static legend label instead.
  • Custom Multi-Series SVG: When a graph requires more than two data series (e.g., four DOP lines or per-constellation SNR lines), write a dedicated PHP function that outputs raw SVG directly rather than using drawGraphSVG(). Custom SVG functions must match the standard graph dimensions (viewBox="0 0 1200 300", $px = 95, $py = 35) for visual consistency. They must also include:
    • Transparent <rect> hit zones with class='graph-hit-zone' and data-time, data-l1/data-v1/data-c1data-l7/data-v7/data-c7 attributes to drive the shared JS tooltip (up to 7 series supported).
    • A <div class='graph-legend-compact'> legend below the SVG using the .legend-series-color + style='--legend-color:var(--token)' pattern for series labels, with per-series avg/min/max popups.
    • Area fills at 4% opacity as a background layer, with polylines rendered on top.
    • Grid lines and axis labels matching the drawGraphSVG() style (5 steps, var(--border) stroke, stroke-dasharray='5,5').
  • Rolling Log Files: New history graphs that require their own log must define a define('FOO_LOG_FILE', '/var/tmp/cgpsd-foo.data') constant and use the same fopen('c+') / flock(LOCK_EX) / read-append-trim-write rolling buffer pattern used by the existing log writers. The trim limit is SAT_LOG_MAX_LINES (15,000 lines). Register new log files in the admin init block’s $dataFiles array so the admin panel can create them on first run.
  • Progress Bars: Use getProgressBar($percent, $text) to render a themed progress bar. It automatically applies .p-bar-ok, .p-bar-warn, or .p-bar-err CSS classes based on the percentage value (>70% → warn, >90% → err), ensuring correct themed colors via var(--status-ok/warn/err). Never build progress bar HTML by hand.
  • Tooltips: If you modify the graphs, ensure you preserve the invisible <rect> “hit zones” that drive the JavaScript tooltips.
  • Log Parsing: The dashboard reads directly from /var/log/chrony. Ensure any new parsers handle file permissions gracefully and fail silently if logs are missing.

2. JavaScript (Vanilla Only)

  • No Frameworks: Use standard ES6+ JavaScript.
  • AJAX Loop: The dashboard uses a single fetch('?ajax=1') loop to update all data.
    • Do not add separate polling intervals for different components.
    • All dynamic data should be returned in the single JSON response.
    • The main poll response is a JSON object. Top-level keys include sources, tracking, gps, graphs, and service_status. If you add a new data field, add it to this response object – do not create a separate endpoint.
    • Other endpoints exist for specific actions (?ajax=version, ?ajax=clients, ?ajax=get_settings, ?ajax=save_settings, ?ajax=regen_admin_token, ?ajax=do_update). Do not repurpose these; add new action keys only when the operation is genuinely distinct from the main poll.
  • DOM Manipulation: Use standard document.getElementById() or querySelector().

3. CSS & Theming

  • Variables: Always use the defined CSS variables (e.g., --bg, --text, --accent, --green) to ensure full Dark/Light mode compatibility.
  • Grid Layout:
    • Mobile First: The default layout is a single-column stack.
    • Desktop: I use @media (min-width: 900px) to switch to a 2-column grid.
    • Full Width Elements: Large graphs should use grid-column: 1 / -1; in the desktop layout to span the full width.
  • UI Styles: ChroGPS Dash has a specific and well-established set of styles, color palettes, etc. Included below is a comprehensive UI style guide developers should reference when contributing.

4. General Development Styles and Guidelines

  • Code should be well-documented/commented
  • No manual minification. The source file must remain fully human-readable. ChroGPS Dash includes a self-minifier that automatically strips comments, collapses whitespace, and removes redundant syntax from the HTML/CSS/JS output buffer at request time – the source is never modified. Never manually minify, uglify, or compress code you contribute; the minifier handles that transparently.
  • PHP code follows PSR-12 guidelines
  • JavaScript follows Modern Vanilla JS (ES6+) guidelines.
    • Indentation should be 4 spaces to maintain consistency with the PHP file structure.
  • CSS and HTML are v3 and v5, respectively.
    • Indentation should be 4 spaces to maintain consistency with the PHP file structure.

5. Hardware Support

  • GPS Types: When parsing gpsd data, aim to support generic NMEA devices as well as specific drivers (u-blox, SiRF).
  • Baud Rates: Preserve the display format @ /dev/path (nnnn baud).

6. Adding New Settings and Configuration Options

ChroGPS Dash uses a single canonical block – $defaultConfig near the very top of index.php – as the source of truth for every user-configurable setting. Understanding how this block works is essential before adding any new option.

How $defaultConfig Works

$defaultConfig is a PHP string that contains the complete, correctly-formatted default contents of cgpsd-settings.php. It serves three purposes simultaneously:

  1. Fresh install: If cgpsd-settings.php does not exist, the dashboard writes $defaultConfig directly to disk to create it.
  2. Shell installer migration: install.sh reads $defaultConfig directly from the downloaded index.php and injects any blocks whose variable is absent from the user’s existing settings file. No separate per-setting maintenance in the installer is required.
  3. Web updater migration (Step 6): The web updater parses $defaultConfig at runtime, extracts every setting block, and injects any that are absent from the user’s live cgpsd-settings.php. This means adding a setting to $defaultConfig is all that is required – the web updater picks it up automatically on the next update for every existing installation worldwide.

The block lives here in index.php:

$defaultConfig = '<?php
// --- OPTIONAL CONFIGURATION ---

// CHECK FOR UPDATES (True/False)
//    ...
$CHECK_UPDATES = true;

// DISPLAY RESOLUTION (Sampling Interval in Seconds)
//    ...
$GRAPH_SAMPLE_SEC = 30;

// ... (one blank line between each setting block) ...

// --- END OPTIONAL CONFIGURATION ---
';

Rules for Adding a New Setting

Follow all of these rules precisely. The web updater’s parser depends on this structure.

1. Add the block to $defaultConfig.

Each setting is a self-contained block consisting of one or more comment lines immediately followed by a single assignment line. Blocks are separated by exactly one blank line.

// SETTING NAME (Type)
//    Description line one.
//    Description line two (optional).
$YOUR_NEW_VAR = <default_value>;

Add it before the // --- END OPTIONAL CONFIGURATION --- line. Increment the number to follow the existing sequence. Use the same escape style as the surrounding single-quoted string – single quotes inside the string must be escaped as \'.

2. No additional changes to install.sh are needed.

The installer’s write_default_config and migrate_settings_from_php functions both read $defaultConfig directly from the downloaded index.php at runtime. There is no separate settings list to maintain in the installer. Adding the block to $defaultConfig is the only step required for the installer to handle the new setting correctly on fresh installs and upgrades alike.

3. Consume the variable in index.php.

Use isset() before referencing your new variable anywhere in the dashboard logic. Settings files from older installations will not have it until the next web update or installer run, so always guard against it being undefined:

// Safe - works whether or not the setting has been injected yet
if (isset($YOUR_NEW_VAR) && $YOUR_NEW_VAR === true) {
    // feature logic
}

4. Document it in your Pull Request.

In your Pull Request, you must create a markdown description of the new setting so that I can update the configuration section of the ChroGPS Dash web page. Follow the same structure as existing entries: variable name as the heading, default value, description, and function (true/false behavior or accepted values).

You also must include the new setting in the example default settings file. Follow the same structure as existing entries in the example.

Warning
If you do not include documentation for the new setting(s), your Pull Request will be silently deleted.

The $skipVars Protected List

The web updater’s Step 6 migration maintains a small $skipVars array of variable names that are never injected during a web update, regardless of whether they are present in the user’s settings file:

$skipVars = ['ADMIN_TOKEN', 'UPDATE_TOKEN'];

ADMIN_TOKEN is protected because anyone running the web updater already has a valid token – injecting a new empty one would lock them out of the admin panel and break web updates entirely. UPDATE_TOKEN is the legacy name for the same credential and is kept in the list for backward compatibility with older installations. Do not add new settings to $skipVars unless there is a specific reason the web updater must never touch them. Variables that need special generation logic at install time (like tokens or secrets) are the only legitimate candidates.

Checklist: Adding a New Setting

  • Block added to $defaultConfig in index.php with correct numbering and formatting
  • No installer changes needed – install.sh reads $defaultConfig from the downloaded index.php at runtime
  • Variable consumed with isset() guard everywhere it is used in dashboard logic
  • Documented in the configuration reference
  • Default value is the most conservative/safe option (features default to false/off)

UI Style Guide

This section is the canonical reference for ChroGPS Dash’s visual design system. All contributors adding or modifying UI elements should follow it to keep the dashboard consistent and maintainable across all twenty themes.

Themes

ChroGPS Dash ships twenty themes, toggled via the data-theme attribute on <html>:

Theme data-theme value Character
Light (default / no attribute) Clean, high-contrast light UI
Dark dark Deep navy backgrounds, bright accents
Terminal terminal Pure black, phosphor-green, dimmed palette
Catppuccin Latte ctp-latte Warm pastel light theme from the Catppuccin palette
Catppuccin Mocha ctp-mocha Muted dark theme from the Catppuccin palette
Tokyo Night tokyo-night Deep blue-black dark theme inspired by Tokyo Night
Nord Dark nord-dark Dark theme using the full Nord palette — Polar Night backgrounds, Frost accents, Aurora status colors
Nord Light nord-light Light theme using the full Nord palette — Snow Storm backgrounds, Polar Night text, Frost accents
Dracula dracula Deep purple-tinted dark theme from Dracula
Gruvbox Dark gruvbox-dark Warm retro-amber dark theme from Gruvbox
Gruvbox Light gruvbox-light Warm retro-amber light theme from Gruvbox
Solarized Dark solarized-dark Precision-toned dark theme with base03 background
Solarized Light solarized-light Precision-toned light theme with base3 background
Rosé Pine rose-pine Muted, dusty dark theme from Rosé Pine
Rosé Pine Dawn rose-pine-dawn Soft, warm light theme from Rosé Pine Dawn
Kanagawa Wave kanagawa-wave Dark blue-gray inspired by a Japanese woodblock painting
Kanagawa Lotus kanagawa-lotus Warm parchment light theme from Kanagawa Lotus
One Dark one-dark Popular dark theme from Atom / VS Code One Dark Pro
One Light one-light Light counterpart from Atom One Light
Monokai monokai Classic Sublime Text Monokai dark theme

Never hardcode a hex color value in CSS or HTML. Always reference a CSS variable. If the color you need is not yet in the palette, add it to all twenty theme blocks first (see Adding New Colors below).

Color Palette

The palette is structured in two layers inside index.php’s <style> block.

Layer 1 – Primitive Color Tokens

Hex color values are defined in two groups within each theme block. Named color primitives are pure hue definitions with no semantic meaning – they are the only tokens you should reference when defining new semantic tokens. A second group of structural tokens also carry direct hex values rather than aliasing a named primitive; these are documented in the second table below.

Named color primitives

All named primitives are defined in every theme block. In Catppuccin Latte, Mocha, and Tokyo Night, --brown, --amber, and --yellow intentionally share the same value – this matches those palettes’ warm-tone roles.

/* Defined in all twenty theme blocks - ordered by hue angle */
--red, --orange, --amber, --yellow, --lime, --green, --cyan,
--blue, --indigo, --purple, --pink,
--brown, --white, --gray
The color wheel

ChroGPS Dash’s named hue tokens are distributed around the HSL color wheel – a 360° circle where 0° = red, 60° = yellow, 120° = green, 180° = cyan, 240° = blue, and 330° = pink before wrapping back to red. The full landmark positions:

Angle Hue
Red
30° Orange
60° Yellow
90° Yellow-green / Lime
120° Green
180° Cyan
240° Blue
270° Purple / Violet
330° Pink / Rose
360° Red (wraps)

Spreading palette colors as far apart as possible on this wheel is what makes them perceptually distinct: two tokens 120° apart will always look clearly different; two tokens only 20-30° apart (like --amber at 40° and --orange at 30°) can appear similar, especially across themes with different saturation levels. This is why tokens like --amber and --yellow are intentionally identical in Catppuccin and Tokyo Night – those palettes assign one warm-tone value to both roles, and the 20° hue gap between them does not survive desaturation.

The table below is ordered by hue angle. The Hue column shows each token’s position on the wheel. When choosing a color for a new component or graph series, pick a token whose hue is as far as possible from any other tokens already in use in the same view.

--red #dc2626 0° · Error / QZSS
--orange #f59e0b 30° · Sun disc fill; sun disc border in Terminal
--amber #d97706 40° · Warning / SBAS
--yellow #eab308 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #65a30d 90° · Frequency Steering graph primary color
--green #16a34a 120° · Success / GPS
--cyan #0891b2 180° · Highlight
--blue #2563eb 240° · Primary accent hue
--indigo #6366f1 250° · NavIC
--purple #9333ea 270° · Galileo / Time fix
--pink #db2777 330° · BeiDou
--brown #b45309 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #ffffff Text on dark bg; dimmed / desaturated per theme
--gray #6b7280 Muted / simulated state
--red #f87171 0° · Error / QZSS
--orange #fbbf24 30° · Sun disc fill; sun disc border in Terminal
--amber #fbbf24 40° · Warning / SBAS
--yellow #fde047 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #a3e635 90° · Frequency Steering graph primary color
--green #4ade80 120° · Success / GPS
--cyan #00e5e5 180° · Highlight
--blue #38bdf8 240° · Primary accent hue
--indigo #818cf8 250° · NavIC
--purple #9333ea 270° · Galileo / Time fix
--pink #f472b6 330° · BeiDou
--brown #d97706 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #ffffff Text on dark bg; dimmed / desaturated per theme
--gray #6b7280 Muted / simulated state
--red #d63232 0° · Error / QZSS
--orange #dd8800 30° · Sun disc fill; sun disc border in Terminal
--amber #e6ac00 40° · Warning / SBAS
--yellow #dddd00 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #aaee00 90° · Frequency Steering graph primary color
--green #00dd00 120° · Success / GPS
--cyan #00dddd 180° · Highlight
--blue #4c88ff 240° · Primary accent hue
--indigo #6655ee 250° · NavIC
--purple #8800cc 270° · Galileo / Time fix
--pink #cc00cc 330° · BeiDou
--brown #aa6600 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #888888 Text on dark bg; dimmed / desaturated per theme
--gray #555555 Muted / simulated state
--red #d20f39 0° · Error / QZSS
--orange #fe640b 30° · Sun disc fill; sun disc border in Terminal
--amber #df8e1d 40° · Warning / SBAS
--yellow #df8e1d 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #6f9400 90° · Frequency Steering graph primary color
--green #40a02b 120° · Success / GPS
--cyan #179299 180° · Highlight
--blue #1e66f5 240° · Primary accent hue
--indigo #7287fd 250° · NavIC
--purple #8839ef 270° · Galileo / Time fix
--pink #ea76cb 330° · BeiDou
--brown #df8e1d ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #eff1f5 Text on dark bg; dimmed / desaturated per theme
--gray #9ca0b0 Muted / simulated state
--red #f38ba8 0° · Error / QZSS
--orange #fab387 30° · Sun disc fill; sun disc border in Terminal
--amber #f9e2af 40° · Warning / SBAS
--yellow #f9e2af 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #c9e08e 90° · Frequency Steering graph primary color
--green #a6e3a1 120° · Success / GPS
--cyan #94e2d5 180° · Highlight
--blue #89b4fa 240° · Primary accent hue
--indigo #b4befe 250° · NavIC
--purple #cba6f7 270° · Galileo / Time fix
--pink #f5c2e7 330° · BeiDou
--brown #f9e2af ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #cdd6f4 Text on dark bg; dimmed / desaturated per theme
--gray #6c7086 Muted / simulated state
--red #f7768e 0° · Error / QZSS
--orange #ff9e64 30° · Sun disc fill; sun disc border in Terminal
--amber #e0af68 40° · Warning / SBAS
--yellow #e0af68 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #b9db5f 90° · Frequency Steering graph primary color
--green #9ece6a 120° · Success / GPS
--cyan #7dcfff 180° · Highlight
--blue #7aa2f7 240° · Primary accent hue
--indigo #8b78f7 250° · NavIC
--purple #9d7cd8 270° · Galileo / Time fix
--pink #f78cb3 330° · BeiDou
--brown #e0af68 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #c0caf5 Text on dark bg; dimmed / desaturated per theme
--gray #565f89 Muted / simulated state
--red #bf616a 0° · Error / QZSS
--orange #d08770 30° · Sun disc fill; sun disc border in Terminal
--amber #ebcb8b 40° · Warning / SBAS
--yellow #ebcb8b 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #96be7a 90° · Frequency Steering graph primary color
--green #a3be8c 120° · Success / GPS
--cyan #8fbcbb 180° · Highlight
--blue #81a1c1 240° · Primary accent hue
--indigo #5e81ac 250° · NavIC
--purple #b48ead 270° · Galileo / Time fix
--pink #cf8fa0 330° · BeiDou
--brown #9a5c3c ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #eceff4 Text on dark bg; dimmed / desaturated per theme
--gray #4c566a Muted / simulated state
--red #bf616a 0° · Error / QZSS
--orange #b05820 30° · Sun disc fill; sun disc border in Terminal
--amber #8a6800 40° · Warning / SBAS
--yellow #8a6800 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #4a7820 90° · Frequency Steering graph primary color
--green #4a7a40 120° · Success / GPS
--cyan #4a8a8a 180° · Highlight
--blue #5e81ac 240° · Primary accent hue
--indigo #5e81ac 250° · NavIC
--purple #7a4a8a 270° · Galileo / Time fix
--pink #a04060 330° · BeiDou
--brown #7a4010 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #ffffff Text on dark bg; dimmed / desaturated per theme
--gray #6b7280 Muted / simulated state
--red #ff5555 0° · Error / QZSS
--orange #ffb86c 30° · Sun disc fill; sun disc border in Terminal
--amber #ffb86c 40° · Warning / SBAS
--yellow #f1fa8c 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #69ff94 90° · Frequency Steering graph primary color
--green #50fa7b 120° · Success / GPS
--cyan #8be9fd 180° · Highlight
--blue #8be9fd 240° · Primary accent hue
--indigo #9aabde 250° · NavIC
--purple #bd93f9 270° · Galileo / Time fix
--pink #ff79c6 330° · BeiDou
--brown #e6994a ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #f8f8f2 Text on dark bg; dimmed / desaturated per theme
--gray #6272a4 Muted / simulated state
--red #fb4934 0° · Error / QZSS
--orange #fe8019 30° · Sun disc fill; sun disc border in Terminal
--amber #fabd2f 40° · Warning / SBAS
--yellow #fabd2f 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #b8bb26 90° · Frequency Steering graph primary color
--green #b8bb26 120° · Success / GPS
--cyan #8ec07c 180° · Highlight
--blue #83a598 240° · Primary accent hue
--indigo #83a598 250° · NavIC
--purple #b16286 270° · Galileo / Time fix
--pink #d3869b 330° · BeiDou
--brown #d65d0e ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #ebdbb2 Text on dark bg; dimmed / desaturated per theme
--gray #928374 Muted / simulated state
--red #9d0006 0° · Error / QZSS
--orange #af3a03 30° · Sun disc fill; sun disc border in Terminal
--amber #b57614 40° · Warning / SBAS
--yellow #b57614 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #79740e 90° · Frequency Steering graph primary color
--green #79740e 120° · Success / GPS
--cyan #427b58 180° · Highlight
--blue #076678 240° · Primary accent hue
--indigo #076678 250° · NavIC
--purple #8f3f71 270° · Galileo / Time fix
--pink #8f3f71 330° · BeiDou
--brown #7a4000 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #f9f5d7 Text on dark bg; dimmed / desaturated per theme
--gray #928374 Muted / simulated state
--red #dc322f 0° · Error / QZSS
--orange #cb4b16 30° · Sun disc fill; sun disc border in Terminal
--amber #b58900 40° · Warning / SBAS
--yellow #b58900 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #859900 90° · Frequency Steering graph primary color
--green #859900 120° · Success / GPS
--cyan #2aa198 180° · Highlight
--blue #268bd2 240° · Primary accent hue
--indigo #6c71c4 250° · NavIC
--purple #6c71c4 270° · Galileo / Time fix
--pink #d33682 330° · BeiDou
--brown #a03a10 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #93a1a1 Text on dark bg; dimmed / desaturated per theme
--gray #586e75 Muted / simulated state
--red #dc322f 0° · Error / QZSS
--orange #cb4b16 30° · Sun disc fill; sun disc border in Terminal
--amber #b58900 40° · Warning / SBAS
--yellow #b58900 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #859900 90° · Frequency Steering graph primary color
--green #859900 120° · Success / GPS
--cyan #2aa198 180° · Highlight
--blue #268bd2 240° · Primary accent hue
--indigo #6c71c4 250° · NavIC
--purple #6c71c4 270° · Galileo / Time fix
--pink #d33682 330° · BeiDou
--brown #a03a10 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #fdf6e3 Text on dark bg; dimmed / desaturated per theme
--gray #93a1a1 Muted / simulated state
--red #eb6f92 0° · Error / QZSS
--orange #f6c177 30° · Sun disc fill; sun disc border in Terminal
--amber #f6c177 40° · Warning / SBAS
--yellow #f6c177 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #3d8c5e 90° · Frequency Steering graph primary color
--green #31748f 120° · Success / GPS
--cyan #9ccfd8 180° · Highlight
--blue #9ccfd8 240° · Primary accent hue
--indigo #c4a7e7 250° · NavIC
--purple #c4a7e7 270° · Galileo / Time fix
--pink #ebbcba 330° · BeiDou
--brown #c07a5a ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #e0def4 Text on dark bg; dimmed / desaturated per theme
--gray #6e6a86 Muted / simulated state
--red #b4637a 0° · Error / QZSS
--orange #ea9d34 30° · Sun disc fill; sun disc border in Terminal
--amber #ea9d34 40° · Warning / SBAS
--yellow #ea9d34 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #3d7a56 90° · Frequency Steering graph primary color
--green #286983 120° · Success / GPS
--cyan #56949f 180° · Highlight
--blue #56949f 240° · Primary accent hue
--indigo #907aa9 250° · NavIC
--purple #907aa9 270° · Galileo / Time fix
--pink #d7827a 330° · BeiDou
--brown #b07055 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #faf4ed Text on dark bg; dimmed / desaturated per theme
--gray #9893a5 Muted / simulated state
--red #e46876 0° · Error / QZSS
--orange #ff9e3b 30° · Sun disc fill; sun disc border in Terminal
--amber #e6c384 40° · Warning / SBAS
--yellow #e6c384 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #76946a 90° · Frequency Steering graph primary color
--green #98bb6c 120° · Success / GPS
--cyan #7aa89f 180° · Highlight
--blue #7e9cd8 240° · Primary accent hue
--indigo #9cabca 250° · NavIC
--purple #957fb8 270° · Galileo / Time fix
--pink #d27e99 330° · BeiDou
--brown #938056 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #dcd7ba Text on dark bg; dimmed / desaturated per theme
--gray #727169 Muted / simulated state
--red #c84053 0° · Error / QZSS
--orange #cc6d00 30° · Sun disc fill; sun disc border in Terminal
--amber #cc6d00 40° · Warning / SBAS
--yellow #77713f 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #6e915f 90° · Frequency Steering graph primary color
--green #6f894e 120° · Success / GPS
--cyan #4e8ca2 180° · Highlight
--blue #4361ac 240° · Primary accent hue
--indigo #624c83 250° · NavIC
--purple #766b90 270° · Galileo / Time fix
--pink #b35b79 330° · BeiDou
--brown #836f4a ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #f2ecbc Text on dark bg; dimmed / desaturated per theme
--gray #716e61 Muted / simulated state
--red #e06c75 0° · Error / QZSS
--orange #d19a66 30° · Sun disc fill; sun disc border in Terminal
--amber #e5c07b 40° · Warning / SBAS
--yellow #e5c07b 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #9ec87a 90° · Frequency Steering graph primary color
--green #98c379 120° · Success / GPS
--cyan #56b6c2 180° · Highlight
--blue #61afef 240° · Primary accent hue
--indigo #7ea7e0 250° · NavIC
--purple #c678dd 270° · Galileo / Time fix
--pink #e06c75 330° · BeiDou
--brown #a07040 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #abb2bf Text on dark bg; dimmed / desaturated per theme
--gray #5c6370 Muted / simulated state
--red #e45649 0° · Error / QZSS
--orange #986801 30° · Sun disc fill; sun disc border in Terminal
--amber #c18401 40° · Warning / SBAS
--yellow #c18401 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #3e8a40 90° · Frequency Steering graph primary color
--green #50a14f 120° · Success / GPS
--cyan #0184bc 180° · Highlight
--blue #4078f2 240° · Primary accent hue
--indigo #3a6eb5 250° · NavIC
--purple #a626a4 270° · Galileo / Time fix
--pink #ca1243 330° · BeiDou
--brown #7a5000 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #ffffff Text on dark bg; dimmed / desaturated per theme
--gray #a0a1a7 Muted / simulated state
--red #f92672 0° · Error / QZSS
--orange #fd971f 30° · Sun disc fill; sun disc border in Terminal
--amber #e6db74 40° · Warning / SBAS
--yellow #e6db74 60° · Sun disc fill in Terminal; warm yellow in other themes
--lime #a6e22e 90° · Frequency Steering graph primary color
--green #a6e22e 120° · Success / GPS
--cyan #66d9e8 180° · Highlight
--blue #66d9e8 240° · Primary accent hue
--indigo #ae81ff 250° · NavIC
--purple #ae81ff 270° · Galileo / Time fix
--pink #f92672 330° · BeiDou
--brown #c07d30 ~30° · Sun disc border; maps to theme yellow in Latte / Mocha / Tokyo Night
--white #f8f8f2 Text on dark bg; dimmed / desaturated per theme
--gray #75715e Muted / simulated state
Global :root-only tokens

The following tokens are defined in :root. Most are not overridden in the per-theme blocks; any exceptions are noted under each section.

Shadow tokens
--shadow-popup   /* per theme - active tab pill, small floating popups */
--shadow-lg      /* per theme - modals, large overlays */
--shadow-sm      /* per theme - minor card/element elevation, small drop shadows */
--shadow-inset   /* per theme - inset depth effect (e.g. progress bar track) */

Always use the appropriate shadow token: --shadow-popup for compact floating elements (pill buttons, inline popups), --shadow-lg for full modals and large overlays, --shadow-sm for minor card or element elevation, and --shadow-inset for inset depth effects. Never hardcode a box-shadow value.

Note
All four shadow tokens are per-theme (not :root-only) – light themes use lighter rgba intensities; dark themes use deeper values. ctp-latte inherits the light theme shadow values since it has a light background.
Easing tokens
--ease-ui     /* cubic-bezier(0.4, 0, 0.2, 1)       - standard Material-style deceleration curve */
--ease-ping   /* cubic-bezier(0, 0, 0.2, 1)          - ping/ripple animation (fast deceleration) */
--ease-spring /* cubic-bezier(0.34, 1.56, 0.64, 1)  - spring/overshoot curve for entrance animations */

Use var(--ease-ui) for all standard transition declarations. Use var(--ease-ping) for ripple/pulse animation keyframes. Use var(--ease-spring) for entrance-style transitions that intentionally overshoot their target (e.g. the modal slide-in). Never write a raw cubic-bezier(...) literal in a rule.

Note
--ease-ping and --ease-spring are redefined in every theme block (values are identical across all themes). This is intentional – it ensures they remain available in the CSS cascade even when a theme block overrides other :root properties.
Badge and button text contrast tokens

These tokens provide per-theme WCAG-compliant text colors for colored badge backgrounds and accent-colored buttons. They are per-theme tokens – each theme block defines its own values to ensure accessible contrast over that theme’s specific badge and button background colors.

--badge-contrast      /* text on standard colored badge backgrounds (fix-type, constellation) */
--badge-time-text     /* text on time/purple badge backgrounds (varies per theme) */
--progress-bar-text   /* text overlaid on progress bar fills (white for Terminal, --text for others) */
--btn-on-accent       /* text on accent-colored buttons (Go, Save, Reload, etc.) */
--badge-contrast #000000
--badge-time-text var(--text)
--progress-bar-text var(--text)
--btn-on-accent #000000
--badge-contrast #000000
--badge-time-text #ffffff
--progress-bar-text var(--text)
--btn-on-accent #000000
--badge-contrast #000000
--badge-time-text var(--text)
--progress-bar-text #ffffff
--btn-on-accent #000000
--badge-contrast #000000
--badge-time-text var(--text)
--progress-bar-text var(--text)
--btn-on-accent #000000
--badge-contrast #000000
--badge-time-text #1e1e2e
--progress-bar-text var(--text)
--btn-on-accent #000000
--badge-contrast #1a1b26
--badge-time-text #1a1b26
--progress-bar-text var(--text)
--btn-on-accent #000000
--badge-contrast #2e3440
--badge-time-text #2e3440
--progress-bar-text var(--text)
--btn-on-accent #2e3440
--badge-contrast #000000
--badge-time-text #eceff4
--progress-bar-text #eceff4
--btn-on-accent #eceff4
--badge-contrast #282a36
--badge-time-text #282a36
--progress-bar-text var(--text)
--btn-on-accent #282a36
--badge-contrast #282828
--badge-time-text #282828
--progress-bar-text var(--text)
--btn-on-accent #282828
--badge-contrast #000000
--badge-time-text #f9f5d7
--progress-bar-text #f9f5d7
--btn-on-accent #f9f5d7
--badge-contrast #002b36
--badge-time-text #002b36
--progress-bar-text var(--text)
--btn-on-accent #002b36
--badge-contrast #000000
--badge-time-text #fdf6e3
--progress-bar-text #fdf6e3
--btn-on-accent #fdf6e3
--badge-contrast #191724
--badge-time-text #191724
--progress-bar-text var(--text)
--btn-on-accent #191724
--badge-contrast #000000
--badge-time-text #fffaf3
--progress-bar-text #fffaf3
--btn-on-accent #fffaf3
--badge-contrast #1f1f28
--badge-time-text #1f1f28
--progress-bar-text var(--text)
--btn-on-accent #1f1f28
--badge-contrast #000000
--badge-time-text #f2ecbc
--progress-bar-text #f2ecbc
--btn-on-accent #f2ecbc
--badge-contrast #282c34
--badge-time-text #282c34
--progress-bar-text var(--text)
--btn-on-accent #282c34
--badge-contrast #000000
--badge-time-text #fafafa
--progress-bar-text #ffffff
--btn-on-accent #ffffff
--badge-contrast #272822
--badge-time-text #272822
--progress-bar-text var(--text)
--btn-on-accent #272822

When adding a new badge type or accent-background button, always use color: var(--badge-contrast) or color: var(--btn-on-accent) – never hardcode #000 or #fff.


Semantic status, fix-type, and update tokens

These tokens are defined once in :root only. They alias the primitive color tokens and automatically inherit each theme’s hue values without needing per-theme overrides.

/* Primary interactive accent */
--accent       /* var(--blue) in most themes; var(--purple) in Dracula, Rosé Pine, Rosé Pine Dawn, Monokai;
                  #88c0d0 in Nord Dark; #5e81ac in Nord Light -- drives buttons, links, active states */

/* Status states */
--status-ok    /* var(--green) - running, connected, healthy */
--status-err   /* var(--red)   - error, failed, not connected */
--status-warn  /* var(--amber) - warning, degraded */
--warn         /* var(--amber) - warning banners and caution panels */

/* GPS fix type */
--fix-3d       /* var(--green) - 3D position fix (Normal) */
--fix-2d       /* var(--amber) - 2D position fix only */
--fix-enhanced /* var(--cyan)  - enhanced 3D fix (DGPS/RTK/DR) */
--fix-none     /* var(--red)   - no position fix */

/* Update availability */
--update-avail /* var(--green) - update-available pill indicator */

Use these semantic tokens – never use var(--green), var(--red), or var(--amber) directly in component rules. Use var(--accent) for any interactive element that should inherit the theme’s primary accent color. If you need to represent a new component state, add a semantic alias to this group in :root rather than reaching for a primitive token directly.


Logo brand tokens

These tokens drive the SVG header logo colorway for each display mode. Do not use them outside the logo SVG.

/* Dark-mode logo */
--logo-d-primary    /* cyan  - solar-panel wings, transmitter, "ChroGPS" text */
--logo-d-secondary  /* cyan-green - orbits, strokes, antenna, "DASH" text */
--logo-d-accent     /* green - blinking dot, orbit dot */

/* Light-mode logo */
--logo-l-primary    /* blue  - solar-panel wings, transmitter */
--logo-l-secondary  /* teal  - orbits, strokes, antenna */
--logo-l-accent     /* green - blinking dot, orbit dot */
--logo-l-text1      /* blue  - "ChroGPS" text */
--logo-l-text2      /* teal  - "DASH" text */

Structural tokens with direct hex values

These tokens are semantic in role but carry direct hex values in the theme blocks rather than aliasing a named color primitive. They appear here for completeness – contributors must be aware of them so they do not accidentally hardcode the same values elsewhere. The same rule applies: never hardcode these hex values in CSS rules or HTML; always reference the token.

A “-” cell means the token is not redefined in that theme (it inherits the :root value). A “var(...)” cell means the token is an alias in that theme.

--bg #f4f4f9 Page background
--card var(--white) Card / panel background
--text #1a1a1a Body text
--border #ddd Dividers, input borders
--badge-text var(--white) Text on colored badge backgrounds
--progress-track #4b5563 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #e2e8f0 Polar plot background
--sky-lines #94a3b8 Elevation ring / crosshair stroke
--sky-label #666666 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #ccc Tooltip border
--tooltip-text #333 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(255, 255, 255, 1) Tooltip / popover background
--pre-bg var(--bg) Code block / <pre> background
--shroud-bg rgba(244, 244, 249, 1) Full-screen loading shroud overlay
--modal-overlay rgba(0, 0, 0, 0.5) Semi-transparent modal backdrop
--bg #0f172a Page background
--card #1e293b Card / panel background
--text #f1f5f9 Body text
--border #334155 Dividers, input borders
--badge-text var(--bg) Text on colored badge backgrounds
--progress-track #334155 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #000000 Polar plot background
--sky-lines var(--border) Elevation ring / crosshair stroke
--sky-label #475569 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #475569 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(30, 41, 59, 1) Tooltip / popover background
--pre-bg var(--bg) Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(0, 0, 0, 0.7) Semi-transparent modal backdrop
--bg #000000 Page background
--card var(--bg) Card / panel background
--text #b7b7b7 Body text
--border #232323 Dividers, input borders
--badge-text #000000 Text on colored badge backgrounds
--progress-track #2a2a2a Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg var(--bg) Polar plot background
--sky-lines #333333 Elevation ring / crosshair stroke
--sky-label #444444 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #333333 Tooltip border
--tooltip-text var(--white) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(0, 0, 0, 0.98) Tooltip / popover background
--pre-bg var(--bg) Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(0, 0, 0, 0.95) Semi-transparent modal backdrop
--bg #eff1f5 Page background
--card #e6e9ef Card / panel background
--text #4c4f69 Body text
--border #acb0be Dividers, input borders
--badge-text #eff1f5 Text on colored badge backgrounds
--progress-track #5c5f77 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #ccd0da Polar plot background
--sky-lines #bcc0cc Elevation ring / crosshair stroke
--sky-label #6c6f85 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #acb0be Tooltip border
--tooltip-text #4c4f69 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(230, 233, 239, 1) Tooltip / popover background
--pre-bg #dce0e8 Code block / <pre> background
--shroud-bg rgba(239, 241, 245, 1) Full-screen loading shroud overlay
--modal-overlay rgba(76, 79, 105, 0.5) Semi-transparent modal backdrop
--bg #1e1e2e Page background
--card #181825 Card / panel background
--text #cdd6f4 Body text
--border #45475a Dividers, input borders
--badge-text #1e1e2e Text on colored badge backgrounds
--progress-track #313244 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #11111b Polar plot background
--sky-lines var(--border) Elevation ring / crosshair stroke
--sky-label #585b70 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #313244 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(24, 24, 37, 1) Tooltip / popover background
--pre-bg #11111b Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(17, 17, 27, 0.8) Semi-transparent modal backdrop
--bg #1a1b26 Page background
--card #16161e Card / panel background
--text #c0caf5 Body text
--border #292e42 Dividers, input borders
--badge-text #1a1b26 Text on colored badge backgrounds
--progress-track #29355a Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #16161e Polar plot background
--sky-lines var(--border) Elevation ring / crosshair stroke
--sky-label #565f89 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #292e42 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(22, 22, 30, 1) Tooltip / popover background
--pre-bg #16161e Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(22, 22, 30, 0.85) Semi-transparent modal backdrop
--bg #2e3440 Page background
--card #3b4252 Card / panel background
--text #eceff4 Body text
--border #4c566a Dividers, input borders
--badge-text #2e3440 Text on colored badge backgrounds
--progress-track #4c566a Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #2e3440 Polar plot background
--sky-lines #434c5e Elevation ring / crosshair stroke
--sky-label #4c566a Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #4c566a Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent #88c0d0 Primary interactive accent; drives buttons, links, active states
--tooltip-bg #3b4252 Tooltip / popover background
--pre-bg #2e3440 Code block / <pre> background
--shroud-bg #2e3440 Full-screen loading shroud overlay
--modal-overlay rgba(46, 52, 64, 0.88) Semi-transparent modal backdrop
--bg #e5e9f0 Page background
--card #eceff4 Card / panel background
--text #2e3440 Body text
--border #d8dee9 Dividers, input borders
--badge-text #eceff4 Text on colored badge backgrounds
--progress-track #4c566a Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #d8dee9 Polar plot background
--sky-lines #81a1c1 Elevation ring / crosshair stroke
--sky-label #4c566a Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #d8dee9 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent #5e81ac Primary interactive accent; drives buttons, links, active states
--tooltip-bg #eceff4 Tooltip / popover background
--pre-bg #e5e9f0 Code block / <pre> background
--shroud-bg #e5e9f0 Full-screen loading shroud overlay
--modal-overlay rgba(46, 52, 64, 0.5) Semi-transparent modal backdrop
--bg #282a36 Page background
--card #44475a Card / panel background
--text #f8f8f2 Body text
--border #6272a4 Dividers, input borders
--badge-text #282a36 Text on colored badge backgrounds
--progress-track #44475a Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #21222c Polar plot background
--sky-lines #44475a Elevation ring / crosshair stroke
--sky-label #6272a4 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #6272a4 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--purple) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(68, 71, 90, 1) Tooltip / popover background
--pre-bg #21222c Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(40, 42, 54, 0.88) Semi-transparent modal backdrop
--bg #282828 Page background
--card #3c3836 Card / panel background
--text #ebdbb2 Body text
--border #504945 Dividers, input borders
--badge-text #282828 Text on colored badge backgrounds
--progress-track #504945 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #1d2021 Polar plot background
--sky-lines #3c3836 Elevation ring / crosshair stroke
--sky-label #504945 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #504945 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(60, 56, 54, 1) Tooltip / popover background
--pre-bg #1d2021 Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(29, 32, 33, 0.88) Semi-transparent modal backdrop
--bg #fbf1c7 Page background
--card #ebdbb2 Card / panel background
--text #3c3836 Body text
--border #d5c4a1 Dividers, input borders
--badge-text #f9f5d7 Text on colored badge backgrounds
--progress-track #504945 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #d5c4a1 Polar plot background
--sky-lines #bdae93 Elevation ring / crosshair stroke
--sky-label #7c6f64 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #d5c4a1 Tooltip border
--tooltip-text #3c3836 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(235, 219, 178, 1) Tooltip / popover background
--pre-bg #f9f5d7 Code block / <pre> background
--shroud-bg rgba(251, 241, 199, 1) Full-screen loading shroud overlay
--modal-overlay rgba(60, 56, 54, 0.5) Semi-transparent modal backdrop
--bg #002b36 Page background
--card #073642 Card / panel background
--text #839496 Body text
--border #073642 Dividers, input borders
--badge-text #002b36 Text on colored badge backgrounds
--progress-track #073642 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #001e26 Polar plot background
--sky-lines #073642 Elevation ring / crosshair stroke
--sky-label #586e75 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #586e75 Tooltip border
--tooltip-text #93a1a1 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(7, 54, 66, 1) Tooltip / popover background
--pre-bg #002b36 Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(0, 43, 54, 0.9) Semi-transparent modal backdrop
--bg #fdf6e3 Page background
--card #eee8d5 Card / panel background
--text #657b83 Body text
--border #d3cbb8 Dividers, input borders
--badge-text #fdf6e3 Text on colored badge backgrounds
--progress-track #586e75 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #eee8d5 Polar plot background
--sky-lines #d3cbb8 Elevation ring / crosshair stroke
--sky-label #93a1a1 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #d3cbb8 Tooltip border
--tooltip-text #657b83 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(238, 232, 213, 1) Tooltip / popover background
--pre-bg #eee8d5 Code block / <pre> background
--shroud-bg rgba(253, 246, 227, 1) Full-screen loading shroud overlay
--modal-overlay rgba(101, 123, 131, 0.5) Semi-transparent modal backdrop
--bg #191724 Page background
--card #1f1d2e Card / panel background
--text #e0def4 Body text
--border #26233a Dividers, input borders
--badge-text #191724 Text on colored badge backgrounds
--progress-track #26233a Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #16141f Polar plot background
--sky-lines #26233a Elevation ring / crosshair stroke
--sky-label #6e6a86 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #403d52 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--purple) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(31, 29, 46, 1) Tooltip / popover background
--pre-bg #16141f Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(22, 20, 31, 0.88) Semi-transparent modal backdrop
--bg #faf4ed Page background
--card #fffaf3 Card / panel background
--text #575279 Body text
--border #dfdad9 Dividers, input borders
--badge-text #fffaf3 Text on colored badge backgrounds
--progress-track #6e6a86 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #f2e9e1 Polar plot background
--sky-lines #dfdad9 Elevation ring / crosshair stroke
--sky-label #9893a5 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #dfdad9 Tooltip border
--tooltip-text #575279 Tooltip body text
--accent var(--purple) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(255, 250, 243, 1) Tooltip / popover background
--pre-bg #f2e9e1 Code block / <pre> background
--shroud-bg rgba(250, 244, 237, 1) Full-screen loading shroud overlay
--modal-overlay rgba(87, 82, 121, 0.5) Semi-transparent modal backdrop
--bg #1f1f28 Page background
--card #16161d Card / panel background
--text #dcd7ba Body text
--border #2a2a37 Dividers, input borders
--badge-text #1f1f28 Text on colored badge backgrounds
--progress-track #2a2a37 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #16161d Polar plot background
--sky-lines #2a2a37 Elevation ring / crosshair stroke
--sky-label #54546d Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #2a2a37 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(22, 22, 29, 1) Tooltip / popover background
--pre-bg #16161d Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(22, 22, 29, 0.88) Semi-transparent modal backdrop
--bg #f2ecbc Page background
--card #e7dba0 Card / panel background
--text #545464 Body text
--border #c8c093 Dividers, input borders
--badge-text #f2ecbc Text on colored badge backgrounds
--progress-track #43436c Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #d5ceac Polar plot background
--sky-lines #c8c093 Elevation ring / crosshair stroke
--sky-label #716e61 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #c8c093 Tooltip border
--tooltip-text #545464 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(231, 219, 160, 1) Tooltip / popover background
--pre-bg #dcd5ac Code block / <pre> background
--shroud-bg rgba(242, 236, 188, 1) Full-screen loading shroud overlay
--modal-overlay rgba(84, 84, 100, 0.5) Semi-transparent modal backdrop
--bg #282c34 Page background
--card #21252b Card / panel background
--text #abb2bf Body text
--border #3e4451 Dividers, input borders
--badge-text #282c34 Text on colored badge backgrounds
--progress-track #3e4451 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #1c2026 Polar plot background
--sky-lines #3e4451 Elevation ring / crosshair stroke
--sky-label #5c6370 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #3e4451 Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(33, 37, 43, 1) Tooltip / popover background
--pre-bg #1c2026 Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(28, 32, 38, 0.88) Semi-transparent modal backdrop
--bg #fafafa Page background
--card #f0f0f0 Card / panel background
--text #383a42 Body text
--border #d3d3d3 Dividers, input borders
--badge-text #ffffff Text on colored badge backgrounds
--progress-track #5c5f66 Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #e8e8e8 Polar plot background
--sky-lines #c8c8c8 Elevation ring / crosshair stroke
--sky-label #a0a1a7 Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #d3d3d3 Tooltip border
--tooltip-text #383a42 Tooltip body text
--accent var(--blue) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(240, 240, 240, 1) Tooltip / popover background
--pre-bg #f0f0f0 Code block / <pre> background
--shroud-bg rgba(250, 250, 250, 1) Full-screen loading shroud overlay
--modal-overlay rgba(56, 58, 66, 0.5) Semi-transparent modal backdrop
--bg #272822 Page background
--card #3e3d32 Card / panel background
--text #f8f8f2 Body text
--border #49483e Dividers, input borders
--badge-text #272822 Text on colored badge backgrounds
--progress-track #49483e Progress bar track -- static dark so white text stays readable over any fill color
--sky-bg #1e1f1c Polar plot background
--sky-lines #49483e Elevation ring / crosshair stroke
--sky-label #75715e Elevation ring degree labels (15 deg, 30 deg...)
--tooltip-border #49483e Tooltip border
--tooltip-text var(--text) Tooltip body text
--accent var(--purple) Primary interactive accent; drives buttons, links, active states
--tooltip-bg rgba(62, 61, 50, 1) Tooltip / popover background
--pre-bg #1e1f1c Code block / <pre> background
--shroud-bg var(--bg) Full-screen loading shroud overlay
--modal-overlay rgba(30, 31, 28, 0.88) Semi-transparent modal backdrop

Adding New Colors

If your feature needs a color that has no existing token:

  1. Add the primitive to all twenty theme blocks (:root, [data-theme="dark"], [data-theme="terminal"], [data-theme="ctp-latte"], [data-theme="ctp-mocha"], [data-theme="tokyo-night"], [data-theme="nord-dark"], [data-theme="nord-light"], [data-theme="dracula"], [data-theme="gruvbox-dark"], [data-theme="gruvbox-light"], [data-theme="solarized-dark"], [data-theme="solarized-light"], [data-theme="rose-pine"], [data-theme="rose-pine-dawn"], [data-theme="kanagawa-wave"], [data-theme="kanagawa-lotus"], [data-theme="one-dark"], [data-theme="one-light"], [data-theme="monokai"]), choosing values appropriate to each theme’s character.
  2. If the color has a semantic role (e.g., it represents a component state), create a semantic token that references the primitive.
  3. Use only the semantic token in your CSS and HTML.
/* ✅ Correct */
.my-element { color: var(--accent); }

/* ❌ Wrong - hardcoded hex breaks all other themes */
.my-element { color: #2563eb; }

/* ❌ Wrong - primitive used directly where a semantic token should exist */
.my-element { color: var(--blue); }

Typography

--font-sans  /* UI text: system sans-serif stack */
--font-mono  /* Code, coordinates, numeric readouts: monospace stack */

Font size everywhere derives from --base-size (default 14px). Use calc(var(--base-size) * N) to scale, never fixed px values for text.

The [data-size] attribute on <html> overrides --base-size at the user’s request: small sets 14px, medium sets 18px, and large sets 22px. All calc(var(--base-size) * N) expressions automatically scale with the selected size.

/* ✅ Correct */
font-size: calc(var(--base-size) * 0.75);   /* small label */
font-size: calc(var(--base-size) * 1.25);   /* section heading */

/* ❌ Wrong */
font-size: 11px;

Monospace text for data values (coordinates, hashes, baud rates, offsets) should use the .font-mono utility class or font-family: var(--font-mono):

<span class="font-mono">2959743f63</span>

Graph SVG typography

PHP-generated SVG graphs follow this rule without exception:

SVG element Font Examples
Axis labels / legend text var(--font-sans) “PPS Offset”, “Sats Active”, Y-axis rotated label
Numeric tick marks, timestamps, value readouts var(--font-mono) 0.123, 14:30, axis min/max values
// ✅ Axis label (sans)
$svg .= "<text ... font-family='var(--font-sans)' ...>Offset</text>";

// ✅ Numeric tick (mono)
$svg .= "<text ... font-family='var(--font-mono)' ...>0.123</text>";
SVG presentation-attribute font-size is exempt from the --base-size rule

SVG font-size attributes (e.g. font-size='12') are SVG user-unit coordinates, not CSS pixel values. They are part of the SVG coordinate system and scale proportionally with the SVG viewport – CSS custom properties cannot be used in SVG presentation attributes from PHP-generated strings. These values are therefore exempt from the calc(var(--base-size) * N) requirement.

When a CSS override is needed to make SVG text readable at a specific viewport size (e.g. in the 2-column graph view), apply it via a scoped CSS rule using calc(var(--base-size) * N):

/* ✅ CSS override on SVG text - must use --base-size */
#tab-panels-wrap.panels-grid .graph-svg text {
    font-size: calc(var(--base-size) * 1.15);
}
// ✅ SVG presentation attribute - user-unit coordinate, exempt from --base-size rule
$svg .= "<text font-size='12' ...>0.123</text>";

// ❌ Wrong - CSS px in a CSS rule, even inside a PHP string
$svg .= "<text style='font-size: 12px' ...>0.123</text>";

Badges

Badges are used for fix-type labels and constellation counts. Always use the .badge base class plus one modifier.

<span class="badge badge-fix">3D Fix</span>
<span class="badge badge-time">Time</span>
<span class="badge badge-amber">2D Fix</span>
<span class="badge badge-nofix">No Fix</span>
<span class="badge badge-cyan">DGPS/RTK</span>
<span class="badge badge-simulated">Simulated</span>
Modifier Color Represents
badge-fix --fix-3d Normal 3D fix
badge-amber --fix-2d 2D fix only
badge-cyan --fix-enhanced Enhanced 3D (DGPS/RTK/DR)
badge-time --purple Time-only precision fix
badge-simulated --gray Simulated / NMEA replay mode
badge-nofix --fix-none No position fix

Do not add style="..." to badge elements. Spacing (margin-right) is handled by the base .badge rule. Text color is handled by --badge-text as the default, with per-theme overrides using --badge-contrast and --badge-time-text for high-contrast cases already in place. If you add a new badge type that needs a contrast override on a specific theme, use color: var(--badge-contrast) in the theme-specific rule block.

Constellation Badges

Constellation count badges use .const-badge plus a .c-* modifier. Both classes are required.

<span class="const-badge c-gps">GPS: 9</span>
<span class="const-badge c-glo">GLONASS: 6</span>
<span class="const-badge c-gal">Galileo: 9</span>
<span class="const-badge c-bds">BeiDou: 5</span>
Modifier Token Constellation
c-gps --sat-gps GPS
c-glo --sat-glo GLONASS
c-gal --sat-gal Galileo
c-bds --sat-bds BeiDou
c-sbas --sat-sbas SBAS
c-qzss --sat-qzss QZSS
c-navic --sat-navic NavIC

Constellation Breakdown Chips

The constellation breakdown is a live JS-rendered component in the skyview card that shows per-constellation used/seen satellite counts. It is populated by the AJAX poll loop — do not render it server-side. The container element is:

<div id="constellation-breakdown" class="constellation-breakdown"></div>

Each chip is built in JavaScript using the following structure. Colors are applied via CSS custom properties — never via direct inline color: or background: properties:

// CSS custom property sets the color; .legend-series-color reads var(--legend-color)
// .const-chip-bar-fill reads var(--chip-color) and var(--chip-pct) from the parent chip
chip.innerHTML = `
  <span class="const-chip-name legend-series-color" style="--legend-color:${meta.color}">${meta.name}</span>
  <span class="const-chip-counts">${used}<span class="op-muted">/${seen}</span></span>
  <div class="const-chip-bar">
    <div class="const-chip-bar-fill" style="--chip-color:${meta.color};--chip-pct:${pct}%"></div>
  </div>
`;

The display order is always [0, 6, 2, 3, 1, 5, 7] (GPS → GLONASS → Galileo → BeiDou → SBAS → QZSS → NavIC) regardless of which constellations are present. Chips for absent constellations are simply skipped. The gnssid values map to the same --sat-* color tokens used in the skyview, SNR histogram, and history graphs — use those tokens consistently.

Info Tags

Info tags are small colored label chips used in data tables and info panels.

<span class="info-tag tag-perfect">Excellent</span>
<span class="info-tag tag-good">Good</span>
<span class="info-tag tag-marginal">Marginal</span>
<span class="info-tag tag-poor">Poor</span>
<span class="info-tag tag-sun">Phenomenon</span>
Modifier Background Use
tag-perfect --green Best-case value
tag-good --accent Normal/acceptable value
tag-marginal --amber Degraded but functional
tag-poor --red Out of spec
tag-sun --sun-fill Solar/equinox event context

Utility Classes

These small single-purpose classes exist to keep inline styles out of HTML. Use them instead of style="..." wherever they apply.

Quality / DOP indicator colors

<span class="clr-good"></span>   <!-- color: var(--green)  -->
<span class="clr-ok"></span>     <!-- color: var(--accent) -->
<span class="clr-fair"></span>   <!-- color: var(--amber)  -->
<span class="clr-poor"></span>   <!-- color: var(--red)    -->

Opacity

<span class="op-full"></span>   <!-- opacity: 1   -->
<span class="op-dim"></span>    <!-- opacity: 0.5 -->
<span class="op-muted"></span>  <!-- opacity: 0.7 -->

Layout & typography

<svg class="icon-inline" ...></svg>        <!-- vertical-align: -3px; margin-right: 6px -->
<span class="font-mono">689e9c61e3</span>  <!-- font-family: var(--font-mono) -->
<span class="font-bold">value</span>       <!-- font-weight: bold -->
<p class="info-note">Contextual note.</p>  <!-- margin-top: 10px; opacity: 0.8 -->

Text color

Use these classes when PHP or JavaScript needs to colorize an inline value or status word without adding a style="" attribute:

<span class="text-accent">value</span>  <!-- color: var(--accent) -->
<span class="text-green">value</span>   <!-- color: var(--green)  -->
<span class="text-amber">value</span>   <!-- color: var(--amber)  -->
<span class="text-red">value</span>     <!-- color: var(--red)    -->
<span class="text-purple">value</span>  <!-- color: var(--purple) -->

Component-specific helpers

These classes exist to eliminate inline styles in specific contexts. Use them rather than adding style="..." to the element.

<!-- Sets tooltip width when used for table-header data-tip hints -->
<div class="tooltip-th">...</div>

<!-- Positions a textarea off-screen for clipboard read; replaces position/opacity/pointer-events inline styles -->
<textarea class="clip-helper"></textarea>

<!-- Bottom-margin spacing for update warning messages -->
<span class="upd-warn-msg">...</span>

<!-- SNR histogram bar state (applied by JS; replaces runtime style.border / style.opacity assignments) -->
<div class="snr-bar snr-bar-used">...</div>   <!-- solid fill, full opacity - used in fix -->
<div class="snr-bar snr-bar-seen">...</div>   <!-- hollow / bordered, slightly dimmed - visible but unused -->

Tooltip System

The dashboard uses two CSS-driven tooltip patterns. Never use the native HTML title= attribute – it is unstyled, inaccessible on touch devices, and breaks the visual consistency of the UI.

has-popup – inline hover tooltips

Use this for any value, label, or metric that benefits from a contextual explanation on hover. The .tip-text element displays a dotted underline to signal interactivity; the .popup div appears on hover.

<div class="has-popup">
    <span class="tip-text">2d 4h 12m</span>
    <div class="popup">
        Chrony running since:<br>
        <span class="font-mono font-bold">2026-03-15 08:42:11 UTC</span>
    </div>
</div>

Popup content supports any documented HTML and utility classes. Use .font-mono for timestamps, hashes, and numeric values; use <strong> or .font-bold for emphasis; use .text-green, .text-amber, or .text-red for status-colored values inside the popup:

<div class="has-popup">
    <span class="tip-text">Root Dispersion</span>
    <div class="popup">
        Current: <span class="font-mono">0.000312 s</span><br>
        Status: <span class="text-green font-bold">Excellent</span>
    </div>
</div>

All popup layout, positioning, and underline styling is handled entirely by the CSS rules for .has-popup, .tip-text, and .popup. Do not add style="..." to any of these elements – if a color variant is needed for a specific context, add a modifier class to the <style> block instead.

data-tip – table header tooltips

Use this on <th> elements to attach a column-description tooltip. JavaScript generates and positions the popup automatically; no extra HTML is needed.

<th data-tip="Signal-to-Noise Ratio in dB-Hz. Higher = stronger signal. &gt;28 Good, &gt;35 Excellent.">SNR</th>

HTML entities must be escaped (&gt;, &lt;, &amp;) since the value lives inside an HTML attribute. Do not use has-popup inside <th> elements – use data-tip exclusively for table headers.


Dashboard View and Card System

The dashboard is divided into collapsible row groups. Each .card element declares which row it belongs to via a data-view attribute. The view selector buttons in the header let users focus on one row at a time or show all.

Card data-view values

Value Row Cards
sources Row 1 Chrony sources table
skyview Row 1 Polar satellite plot
tracking Row 2 Tracking metrics
snr Row 2 SNR histogram
history Row 3 History graphs (full-width)

When adding a new card, set its data-view to the appropriate row value. The card will then appear and disappear correctly when users switch views.

<div class="card" data-view="tracking">
    <!-- new tracking-row card content -->
</div>

View selector IDs

The header view buttons use data-view on <button> elements. The available view IDs are all, row1, row2, graphs (row 3 / history), and cycle (auto-rotate through rows). Do not add new top-level view IDs without a compelling reason – the three-row model maps directly to the dashboard’s logical sections (Sources / Tracking / History).


PHP-Generated Colors

PHP cannot read CSS variable values at render time, but SVG attributes do accept var(--token) references, and the browser resolves them correctly. Prefer CSS variable references in SVG attributes over hardcoded hex wherever possible:

// ✅ Preferred - CSS variable resolves correctly in SVG
$svg .= "<polyline stroke='var(--accent)' .../>";
$svg .= "<text fill='var(--purple)' ...>Sats Active</text>";

// ✅ Required for <stop> elements (SVG spec mandates style attribute)
$svg .= "<stop offset='0%' style='stop-color:#16a34a'/>";

// ❌ Avoid - hardcoded hex breaks all non-light themes
$svg .= "<polyline stroke='#38bdf8' .../>";

For colors that must be driven by runtime state in PHP (e.g., progress-bar fills), use a CSS modifier class rather than a hardcoded value. The progress bar thresholds are a good example: getProgressBar() applies .p-bar-ok, .p-bar-warn, or .p-bar-err so the fill color comes from var(--status-ok), var(--status-warn), or var(--status-err) respectively – no hex value ever enters the HTML.

When a PHP- or JS-generated element needs a truly dynamic color that cannot be expressed as a fixed CSS class (e.g., a per-series graph color passed as a parameter), use a CSS custom property declaration rather than a direct property assignment:

// ✅ CSS custom property declaration -- value is injected, styling stays in the stylesheet
"<strong class='legend-series-color' style='--legend-color: $colorY2;'>$label</strong>"

// ❌ Avoid - direct property assignment is an inline style
"<strong style='color: $colorY2;'>$label</strong>"

The corresponding CSS rule (color: var(--legend-color)) lives in the <style> block. This keeps all styling declarations in one place while still supporting runtime-computed values.

CSS Authoring Rules

  1. No inline style="..." in HTML unless the value is truly runtime-dynamic (e.g., a bar width computed from live data). Layout, color, spacing, and typography all belong in the <style> block. When a runtime-dynamic color is unavoidable, use a CSS custom property declaration (style="--foo: ${val}") paired with a CSS class that reads it (color: var(--foo)), rather than assigning the property directly inline. JavaScript .style.* assignments are only acceptable for values computed at runtime from data (e.g., tooltip position offsets); static values must use CSS classes instead.

  2. No hardcoded hex values outside the primitive color token definitions. This applies to CSS rules, HTML attributes, and PHP-generated SVG attributes – use CSS variable references (var(--token)) exclusively.

  3. Use semantic tokens, not primitives, in rules. Write color: var(--accent) not color: var(--blue). The primitive exists to define the value; the semantic token expresses the intent.

  4. Modifiers over duplication. If a component needs a variant, add a modifier class (e.g., .info-p.mt-14) rather than copying the base rule with one property changed.

  5. New components need CSS rules in all twenty themes. Test every new element by cycling through Light → Dark → Terminal → Catppuccin Latte → Catppuccin Mocha → Tokyo Night → Nord Dark → Nord Light → Dracula → Gruvbox Dark → Gruvbox Light → Solarized Dark → Solarized Light → Rosé Pine → Rosé Pine Dawn → Kanagawa Wave → Kanagawa Lotus → One Dark → One Light → Monokai before submitting.

  6. SVG <stop> elements are the only legitimate exception to the no-inline-style rule – the SVG specification requires stop-color to be declared as a style attribute. SVG elements that are not <stop> should use stroke="var(--sky-lines)" and similar CSS variable references directly in the attribute.

  7. Use the appropriate easing token for all transitions and animations. var(--ease-ui) for standard UI transitions; var(--ease-ping) for ripple/pulse animation keyframes; var(--ease-spring) for entrance-style spring animations that intentionally overshoot. Never write a raw cubic-bezier(...) literal in a rule.

  8. Use the appropriate shadow token for all shadows. Compact floating elements (pill buttons, inline popups) use --shadow-popup; full modals and large overlays use --shadow-lg; minor card/element elevation uses --shadow-sm; inset depth effects (e.g. progress bar track) use --shadow-inset. Never hardcode a box-shadow value.

  9. No consecutive blank lines in the <style> block. A single blank line between blocks is the maximum. This keeps the source readable and consistent.

  10. Tab buttons share a common base rule. The selectors .legend-tab-btn, .adm-tab-btn, and .tab-btn are merged into a single grouped rule. Any new tab-style button must use one of these existing classes rather than introducing a new selector with duplicated properties.

  11. Write minifier-safe code. The self-minifier processes the output buffer using a character-by-character state machine. A few patterns require care:

    • calc() is fully protected – the minifier stashes all calc() expressions before any whitespace collapse and restores them verbatim, so calc(var(--base-size) * 0.75) is always safe.
    • JS template literals are fully protected – anything inside backticks is passed through verbatim. Multi-line template strings that build HTML are safe.
    • JS // single-line comments – the minifier preserves the terminating newline, so the next line of code is never merged into the comment. Standard comment style is safe.
    • JS regex literals – genuine regex literals (e.g. /pattern/flags) are preserved by the state machine. Avoid constructing regex-like strings in contexts where the parser may misread them; prefer new RegExp(...) for dynamic patterns.
    • Never use <?php ... ?> open/close tags inside an already-open PHP block – the minifier functions are injected into the PHP execution context. Spurious <?php tags within that context will cause a parse error.

Attribution is required under the MIT License. Happy coding!

Document Version: 7fba484 -- Last Revision: 2026-03-29
Permanent Link: <https://w0chp.radio/chrogps-dash/contributing/>