Skip to content

Commit 1aa7738

Browse files
authored
Merge pull request #25 from davidhuser/dev
UI changes
2 parents caac30a + 65a7eeb commit 1aa7738

9 files changed

Lines changed: 62 additions & 330 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# MXmap — Email Providers of Swiss Municipalities
1+
# MXmap — municipal email infrastructure maps
22

33
[![CI](https://github.com/davidhuser/mxmap/actions/workflows/ci.yml/badge.svg)](https://github.com/davidhuser/mxmap/actions/workflows/ci.yml)
44

5-
An interactive map showing where Swiss municipalities host their email — whether with US hyperscalers (Microsoft, Google, AWS) or Swiss providers or other solutions.
5+
Interactive maps showing where Swiss municipalities host their email and how deeply their DNS is tied to US hyperscalers (Microsoft, Google, AWS) versus Swiss providers and self-hosted solutions.
66

7-
**[View the live map](https://mxmap.ch)**
7+
**[View the live maps](https://mxmap.ch)**
88

99
[![Screenshot of MXmap](og-image.jpg)](https://mxmap.ch)
1010

css/map.css

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ body { display: flex; flex-direction: column; height: 100dvh; overflow: hidden;
1111
#info-bar {
1212
background: #f0f1f5; border-bottom: 1px solid #d8dae0; overflow: clip; flex-shrink: 0;
1313
position: relative; z-index: 1;
14-
max-height: 600px; padding: 16px 20px;
14+
min-height: 120px; max-height: 600px; padding: 13px 20px;
1515
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
1616
}
17-
#info-bar.collapsed { max-height: 0; padding-top: 0; padding-bottom: 0; }
17+
#info-bar.collapsed { min-height: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
1818

1919
.info-grid {
2020
display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
21-
gap: 14px;
21+
gap: 12px;
2222
}
2323
.info-card h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; color: #1a1a2e; }
2424
.info-card p { font-size: 13px; color: #444; line-height: 1.5; margin: 0; }
2525
.info-card a { color: #2563eb; text-decoration: none; }
2626
.info-card a:hover { text-decoration: underline; }
27-
#generated { font-size: 11px; color: #999; margin-top: 4px; }
27+
#generated { font-size: 12px; color: #999; margin-top: 4px; }
2828
.info-title { display: none; font-size: 16px; font-weight: 600; color: #1a1a2e; margin-bottom: 12px; }
2929

3030
.legend-toggle {
@@ -42,8 +42,12 @@ body { display: flex; flex-direction: column; height: 100dvh; overflow: hidden;
4242
.legend-group { margin-top: 8px; }
4343
.legend-group:first-child { margin-top: 4px; }
4444
.legend-group-label {
45-
font-size: 11px; color: #999; text-transform: uppercase;
46-
letter-spacing: 0.3px; font-weight: 600; display: block; margin-bottom: 2px;
45+
font-size: 13px; color: #333;
46+
letter-spacing: 0.3px; font-weight: 600; display: block; margin-bottom: 4px;
47+
}
48+
.legend-other {
49+
margin-top: 10px; padding-top: 8px; border-top: 1px solid #e5e7eb;
50+
font-size: 12px; color: #555;
4751
}
4852

4953
#map { flex: 1; position: relative; width: 100%; will-change: transform; }
@@ -108,7 +112,7 @@ body { display: flex; flex-direction: column; height: 100dvh; overflow: hidden;
108112
.cat-badge.unknown { background: #f3f4f6; color: #6b7280; }
109113

110114
.color-toggle {
111-
display: block; margin-top: 8px; padding: 3px 8px; border-radius: 3px;
115+
display: block; margin: 8px auto 0; padding: 3px 8px; border-radius: 3px;
112116
font-size: 11px; font-weight: 500; cursor: pointer;
113117
background: #f3f4f6; border: 1px solid #d1d5db; color: #374151;
114118
}

css/shared.css

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,24 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-
88
position: relative; z-index: 2000;
99
}
1010
.header-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
11-
.brand { font-size: 17px; font-weight: 700; white-space: nowrap; margin: 0; }
11+
.brand {
12+
font-size: 14px; font-weight: 700; white-space: nowrap; margin: 0;
13+
line-height: 1.05;
14+
}
1215
.brand a { color: #fff; text-decoration: none; }
13-
.beta-badge {
14-
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
15-
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
16-
color: #f0c040; padding: 1px 6px; border-radius: 3px; vertical-align: middle;
17-
cursor: default;
16+
.brand-tag {
17+
display: block; font-weight: 400; font-size: 11px;
18+
opacity: 0.6; letter-spacing: 0.3px; margin-top: 1px;
1819
}
1920
.header-right { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
2021
.header-disclaimer {
21-
flex: 1; text-align: center; font-size: 12px; color: #f6ed95;
22+
flex: 1; text-align: center; font-size: 12px; color: #fff37f;
2223
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
2324
padding: 0 12px;
2425
}
2526

2627
#nav { display: flex; align-items: center; gap: 8px; position: relative; margin-left: 6px; }
2728

28-
.nav-primary { display: flex; align-items: center; gap: 6px; }
29-
.nav-primary .header-link {
30-
font-size: 13px; padding: 5px 14px; border-radius: 4px; font-weight: 500;
31-
border: 1px solid rgba(255,255,255,0.3); color: #ccc; text-decoration: none;
32-
}
33-
.nav-primary .header-link:hover {
34-
background: rgba(255,255,255,0.15); color: #fff;
35-
}
36-
.nav-primary .header-link.active {
37-
background: rgba(255,255,255,0.2); color: #fff; font-weight: 600;
38-
border-color: rgba(255,255,255,0.5);
39-
}
40-
4129
.nav-menu-toggle {
4230
background: none; border: none; color: #a0a0b0; font-size: 18px;
4331
cursor: pointer; padding: 2px 4px; line-height: 1;
@@ -56,12 +44,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-
5644
.nav-menu .header-link:hover { color: #fff; }
5745
.nav-menu .header-link.active { color: #fff; font-weight: 600; }
5846
.nav-menu .header-link:last-child { border-bottom: none; }
59-
.nav-menu .nav-menu-mobile { display: none; }
6047

6148
@media (max-width: 600px) {
62-
.beta-badge { display: none; }
63-
.header-disclaimer { font-size: 10px; padding: 0 6px; }
64-
.nav-primary .header-link { font-size: 11px; padding: 2px 8px; }
65-
.nav-primary { display: none; }
66-
.nav-menu .nav-menu-mobile { display: block; }
49+
.header-disclaimer { display: none; }
6750
}

datenschutz.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<meta name="robots" content="noindex, follow">
7-
<title>Datenschutzerklärung MXmap</title>
7+
<title>Datenschutzerklärung | MXmap</title>
88
<link rel="stylesheet" href="css/shared.css">
99
<link rel="stylesheet" href="css/content.css">
1010
<script defer src="js/nav.js"></script>
1111
</head>
1212
<body>
1313
<header id="header">
1414
<div class="header-left">
15-
<h1 class="brand"><a href="/">MXmap</a></h1>
15+
<h1 class="brand"><a href="/">MXmap<span class="brand-tag">municipal email infrastructure</span></a></h1>
1616
<nav id="nav"></nav>
1717
</div>
1818
</header>
@@ -29,8 +29,8 @@ <h2>2. Hosting</h2>
2929
<h2>3. Externe Ressourcen (CDNs)</h2>
3030
<p>Zur Darstellung der Karte werden externe Ressourcen von Drittanbietern geladen. Dabei wird Ihre IP-Adresse an die jeweiligen Server übermittelt:</p>
3131
<ul>
32-
<li><code>unpkg.com</code> Leaflet-Kartenbibliothek und Schweizer Gemeindegrenzen (TopoJSON)</li>
33-
<li><code>basemaps.cartocdn.com</code> Kartenkacheln (CARTO)</li>
32+
<li><code>unpkg.com</code>: Leaflet-Kartenbibliothek und Schweizer Gemeindegrenzen (TopoJSON)</li>
33+
<li><code>basemaps.cartocdn.com</code>: Kartenkacheln (CARTO)</li>
3434
</ul>
3535

3636
<h2>4. Cookies</h2>

impressum.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<meta name="robots" content="noindex, follow">
7-
<title>Impressum MXmap</title>
7+
<title>Impressum | MXmap</title>
88
<link rel="stylesheet" href="css/shared.css">
99
<link rel="stylesheet" href="css/content.css">
1010
<script defer src="js/nav.js"></script>
1111
</head>
1212
<body>
1313
<header id="header">
1414
<div class="header-left">
15-
<h1 class="brand"><a href="/">MXmap</a></h1>
15+
<h1 class="brand"><a href="/">MXmap<span class="brand-tag">municipal email infrastructure</span></a></h1>
1616
<nav id="nav"></nav>
1717
</div>
1818
</header>

index.html

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@
1313
<link rel="dns-prefetch" href="https://b.basemaps.cartocdn.com">
1414
<link rel="dns-prefetch" href="https://c.basemaps.cartocdn.com">
1515
<link rel="dns-prefetch" href="https://d.basemaps.cartocdn.com">
16-
<title>MXmap Email Providers of Swiss Municipalities</title>
16+
<title>MXmap | Email Providers of Swiss Municipalities</title>
1717
<meta name="description" content="Interactive map showing the email providers of all ~2,100 Swiss municipalities, based on DNS analysis of MX and SPF records.">
1818
<meta name="keywords" content="Switzerland, municipalities, email, sovereignty, CLOUD Act, MX records, SPF, DNS, Microsoft 365, Google Workspace, digital sovereignty, data protection">
1919
<meta name="author" content="David Huser">
2020
<meta name="robots" content="index, follow">
2121
<link rel="canonical" href="https://mxmap.ch/">
2222
<meta property="og:type" content="website">
23-
<meta property="og:title" content="MXmap Email Providers of Swiss Municipalities">
23+
<meta property="og:title" content="MXmap | Email Providers of Swiss Municipalities">
2424
<meta property="og:description" content="Interactive map showing where Swiss municipalities host their official email. DNS analysis of all ~2,100 municipalities, color-coded by provider.">
2525
<meta property="og:url" content="https://mxmap.ch/">
2626
<meta property="og:locale" content="en_CH">
2727
<meta property="og:site_name" content="MXmap">
2828
<meta property="og:image" content="https://mxmap.ch/og-image.jpg">
2929
<meta name="twitter:card" content="summary_large_image">
30-
<meta name="twitter:title" content="MXmap Email Providers of Swiss Municipalities">
30+
<meta name="twitter:title" content="MXmap | Email Providers of Swiss Municipalities">
3131
<meta name="twitter:description" content="Interactive map showing where Swiss municipalities host their official email. DNS analysis of all ~2,100 municipalities.">
3232
<meta name="twitter:image" content="https://mxmap.ch/og-image.jpg">
3333
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='2' y='7' width='28' height='18' rx='2' fill='%231a1a2e'/%3E%3Cpath d='M2 9l14 9 14-9' fill='none' stroke='%2316a34a' stroke-width='2'/%3E%3C/svg%3E">
@@ -43,11 +43,10 @@
4343
<body>
4444
<header id="header">
4545
<div class="header-left">
46-
<h1 class="brand">MXmap</h1>
47-
<span class="beta-badge" title="Beta status, this website may contain errors">beta</span>
46+
<h1 class="brand">MXmap<span class="brand-tag">municipal email infrastructure</span></h1>
4847
<nav id="nav"></nav>
4948
</div>
50-
<span class="header-disclaimer">&#9888; This data may be out of date or incorrect. A research project is ongoing to further develop such maps.</span>
49+
<span class="header-disclaimer">&#9888; <strong>Beta:</strong> data may be out of date or incorrect. A research project is ongoing to further develop these maps.</span>
5150
<div class="header-right">
5251
<button class="toggle-info" id="toggle-info" aria-expanded="true" aria-controls="info-bar">About &#9652;</button>
5352
</div>
@@ -58,21 +57,23 @@ <h2 class="info-title">Email Providers of Swiss Municipalities</h2>
5857
<div class="info-grid">
5958
<div class="info-card">
6059
<h3>What is this?</h3>
61-
<p>A map of all ~2,100 Swiss municipalities showing which provider handles their official email — grouped by jurisdiction — based on public DNS records and other public network signals.</p>
60+
<p>A map of all ~2,100 Swiss municipalities, grouped by jurisdiction. Each is colored by the provider handling its official email, based on public DNS records and other public network signals.</p>
6261
</div>
6362
<div class="info-card">
6463
<h3>Context</h3>
65-
<p>Digital sovereignty: US-based providers are subject to the US CLOUD Act, which allows US authorities to request stored data, regardless of where it is physically hosted. This map makes the current provider landscape visible.</p>
64+
<p>US-based providers are subject to the US CLOUD Act, allowing US authorities to request stored data regardless of where it is hosted. This map makes the current provider landscape visible.</p>
6665
</div>
6766
<div class="info-card">
6867
<h3>How does it work?</h3>
69-
<p>Each municipality's official domain is checked via 11 signals from DNS records, SMTP banners, ASN lookups, and a public Microsoft API endpoint, then classified by provider type with confidence scoring.</p>
70-
<p style="font-size:12px;color:#777;margin-top:6px;">Disclaimer: DNS records indicate mail routing and authorized senders, not necessarily where data is stored.</p>
68+
<p>Each municipality's email domain is checked against multiple public DNS and network signals, then classified by provider type.</p>
69+
</div>
70+
<div class="info-card">
71+
<h3>Accuracy</h3>
72+
<p>Classifications may contain errors. DNS records indicate mail routing, not necessarily where data is stored. <br>If you notice an error, please <a href="https://github.com/davidhuser/mxmap/issues" target="_blank">submit an issue</a>.</p>
7173
</div>
7274
<div class="info-card">
7375
<h3>Open source &amp; open data</h3>
74-
<p>The code and data are on <a href="https://github.com/davidhuser/mxmap" target="_blank">GitHub</a>.<br>
75-
If you have noticed an error, please <a href="https://github.com/davidhuser/mxmap/issues" target="_blank">submit an issue</a>.</p>
76+
<p>The code and data are on <a href="https://github.com/davidhuser/mxmap" target="_blank">GitHub</a>.</p>
7677
<p id="generated"></p>
7778
</div>
7879
</div>
@@ -85,9 +86,9 @@ <h3>Open source &amp; open data</h3>
8586
document.addEventListener('DOMContentLoaded', function () {
8687
const COLOR_SCHEMES = {
8788
default: {
88-
'us-cloud': { high: '#ffa199', medium: '#ffccb6', low: '#cccccc' },
89+
'us-cloud': { high: '#ff9e91', medium: '#ffccaa', low: '#cccccc' },
8990
'swiss-based': { high: '#88faaa', medium: '#daffc2', low: '#cccccc' },
90-
lake: '#89B3D6',
91+
lake: '#a3cef1',
9192
},
9293
colorblind: {
9394
'us-cloud': { high: '#f1be61', medium: '#fde7c4', low: '#cccccc' },
@@ -346,7 +347,6 @@ <h3>Open source &amp; open data</h3>
346347
}
347348
}
348349

349-
350350
const map = initMap('map');
351351
setupInfoBar(map);
352352
const isMobile = window.innerWidth <= 600;
@@ -377,7 +377,11 @@ <h3>Open source &amp; open data</h3>
377377
}
378378

379379
document.querySelectorAll('.legend-swatch').forEach(function (el) {
380-
el.style.background = CATEGORY_COLORS[el.dataset.cat][el.dataset.level];
380+
if (el.dataset.lake) {
381+
el.style.background = CATEGORY_COLORS.lake;
382+
} else {
383+
el.style.background = CATEGORY_COLORS[el.dataset.cat][el.dataset.level];
384+
}
381385
});
382386

383387
var btn = document.querySelector('.color-toggle');
@@ -399,7 +403,7 @@ <h3>Open source &amp; open data</h3>
399403
showGenerated(dnsData);
400404

401405
// Count by jurisdiction and confidence level
402-
const catCounts = { 'us-cloud': 0, 'swiss-based': 0, 'insufficient': 0 };
406+
const catCounts = { 'us-cloud': 0, 'swiss-based': 0 };
403407
const levelCounts = {
404408
'us-cloud': { high: 0, medium: 0, low: 0 },
405409
'swiss-based': { high: 0, medium: 0, low: 0 }
@@ -411,14 +415,9 @@ <h3>Open source &amp; open data</h3>
411415
catCounts[cat]++;
412416
const level = confidenceLevel(m ? (m.classification_confidence || 0) : 0);
413417
if (level !== 'insufficient') levelCounts[cat][level]++;
414-
} else {
415-
catCounts['insufficient']++;
416418
}
417419
}
418420

419-
const hatchSvg = 'data:image/svg+xml,' +
420-
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8" fill="#BFBFBF"/><path d="M-1,1 l2,-2 M0,8 l8,-8 M7,9 l2,-2" stroke="#000" stroke-width="1"/></svg>');
421-
422421
// Legend with counts
423422
const isMobile = window.innerWidth <= 600;
424423
const legend = L.control({ position: isMobile ? 'topright' : 'bottomright' });
@@ -428,10 +427,11 @@ <h3>Open source &amp; open data</h3>
428427
div.innerHTML =
429428
'<button class="legend-toggle" aria-label="Toggle legend" aria-expanded="' + (!isMobile) + '">' + (isMobile ? 'Legend \u25B8' : '') + '</button>' +
430429
'<div class="legend-content">' +
431-
'<strong>Email Jurisdiction</strong>' +
432430
legendCategoryHtml('US Cloud', CATEGORY_COLORS['us-cloud'], catCounts['us-cloud'], 'us-cloud', levelCounts['us-cloud']) +
433431
legendCategoryHtml('Swiss Based', CATEGORY_COLORS['swiss-based'], catCounts['swiss-based'], 'swiss-based', levelCounts['swiss-based']) +
434-
'<div class="legend-group"><i style="background-image:url(\'' + hatchSvg + '\')"></i>Insufficient data (' + catCounts['insufficient'] + ')</div>' +
432+
'<div class="legend-group legend-other">' +
433+
'<i class="legend-swatch" data-lake="1" style="background:' + CATEGORY_COLORS.lake + '"></i>Lake (not a municipality)' +
434+
'</div>' +
435435
'<button class="color-toggle" aria-label="Switch to colorblind-safe colors">\u25D0 Colorblind mode</button>' +
436436
'</div>';
437437
L.DomEvent.disableClickPropagation(div);

js/map-shared.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* map-shared.js shared utilities for map pages */
1+
/* map-shared.js - shared utilities for map pages */
22

33
function escapeHtml(str) {
44
var el = document.createElement('span');
@@ -79,7 +79,7 @@ function showGenerated(dnsData) {
7979
var date = new Date(dnsData.generated);
8080
var text = 'Updated ' + date.toLocaleString('de-CH', { dateStyle: 'medium', timeStyle: 'short' });
8181
if (dnsData.commit) {
82-
text += ' \u00b7 ' + dnsData.commit;
82+
text += ' \u00b7 commit ' + dnsData.commit;
8383
}
8484
document.getElementById('generated').textContent = text;
8585
}

js/nav.js

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,35 @@
1-
/* nav.js auto-renders navigation with pill tabs + dropdown overflow menu */
1+
/* nav.js - auto-renders dropdown overflow menu */
22
(function () {
33
var path = window.location.pathname;
4-
var primary = [
5-
{ href: '/', label: 'Email Map', match: ['/', '/index.html'] },
6-
{ href: '/tenant.html', label: 'Tenant Map' },
7-
];
8-
var secondary = [
4+
var links = [
95
{ href: '/impressum.html', label: 'Impressum' },
106
{ href: '/datenschutz.html', label: 'Datenschutz' },
117
];
128

139
var nav = document.getElementById('nav');
1410
if (!nav) return;
1511

16-
function isActive(link) {
17-
return link.match
18-
? link.match.indexOf(path) !== -1
19-
: path === link.href;
20-
}
21-
22-
function makeLink(link, extraClass) {
12+
function makeLink(link) {
2313
var a = document.createElement('a');
2414
a.href = link.href;
25-
a.className = 'header-link' + (extraClass ? ' ' + extraClass : '');
15+
a.className = 'header-link';
2616
a.textContent = link.label;
27-
if (isActive(link)) a.classList.add('active');
17+
if (path === link.href) a.classList.add('active');
2818
return a;
2919
}
3020

31-
/* inline primary links as pill tabs (hidden on mobile via CSS) */
32-
var inlineWrap = document.createElement('span');
33-
inlineWrap.className = 'nav-primary';
34-
primary.forEach(function (link) {
35-
inlineWrap.appendChild(makeLink(link));
36-
});
37-
nav.appendChild(inlineWrap);
38-
3921
/* toggle button */
4022
var toggle = document.createElement('button');
4123
toggle.className = 'nav-menu-toggle';
4224
toggle.setAttribute('aria-label', 'More links');
4325
toggle.setAttribute('aria-expanded', 'false');
44-
toggle.textContent = '\u22EF';
26+
toggle.textContent = '';
4527
nav.appendChild(toggle);
4628

4729
/* dropdown menu */
4830
var menu = document.createElement('div');
4931
menu.className = 'nav-menu';
50-
primary.forEach(function (link) {
51-
menu.appendChild(makeLink(link, 'nav-menu-mobile'));
52-
});
53-
secondary.forEach(function (link) {
32+
links.forEach(function (link) {
5433
menu.appendChild(makeLink(link));
5534
});
5635
nav.appendChild(menu);

0 commit comments

Comments
 (0)