feat(map): quiet HUD chrome — VIIRS default, collapsed layer rail #49

Merged
sirius merged 2 commits from feat/hud-chrome-quiet into master 2026-09-01 17:21:10 -04:00
2 changed files with 298 additions and 170 deletions

View file

@ -3,10 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>OSINT//DASHBOARD — Global Situational Awareness Terminal</title> <title>OSINT//</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css"> <link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="/static/vendor/leaflet/MarkerCluster.css"> <link rel="stylesheet" href="/static/vendor/leaflet/MarkerCluster.css">
<link rel="stylesheet" href="/static/vendor/leaflet/MarkerCluster.Default.css"> <link rel="stylesheet" href="/static/vendor/leaflet/MarkerCluster.Default.css">
@ -35,12 +35,12 @@
--text: #c8e6f2; --text: #c8e6f2;
--muted: #5f7d95; --muted: #5f7d95;
--topbar-h: 46px; --topbar-h: 46px;
--dock-h: 62px; --dock-h: 32px; /* one NEWS ticker; MKT hidden until live */
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; } html, body { height: 100%; }
body { body {
font-family: 'Rajdhani', 'Segoe UI', system-ui, sans-serif; font-family: 'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif;
background: var(--bg-0); color: var(--text); background: var(--bg-0); color: var(--text);
overflow: hidden; overflow: hidden;
font-size: 15px; letter-spacing: 0.01em; font-size: 15px; letter-spacing: 0.01em;
@ -48,12 +48,12 @@
::selection { background: var(--cyan-dim); color: #eafcff; } ::selection { background: var(--cyan-dim); color: #eafcff; }
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; } :focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
a { color: var(--cyan); } a { color: var(--cyan); }
.mono, code, kbd { font-family: 'Share Tech Mono', ui-monospace, SFMono-Regular, Menlo, monospace; } .mono, code, kbd { font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; }
.btn { .btn {
background: linear-gradient(180deg, var(--bg-3), var(--bg-2)); background: linear-gradient(180deg, var(--bg-3), var(--bg-2));
border: 1px solid var(--line); color: var(--text); border: 1px solid var(--line); color: var(--text);
border-radius: 4px; padding: 0.35rem 0.9rem; cursor: pointer; border-radius: 4px; padding: 0.35rem 0.9rem; cursor: pointer;
font-family: 'Rajdhani', sans-serif; font-weight: 600; font-family: 'IBM Plex Sans', sans-serif; font-weight: 600;
font-size: 0.8rem; letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.06em; text-transform: uppercase;
transition: border-color .15s, box-shadow .15s, color .15s; transition: border-color .15s, box-shadow .15s, color .15s;
} }
@ -84,18 +84,18 @@
} }
.brand { display: flex; align-items: center; gap: 0.65rem; min-width: 0; } .brand { display: flex; align-items: center; gap: 0.65rem; min-width: 0; }
.brand-mark { .brand-mark {
font-family: 'Orbitron', sans-serif; color: var(--cyan); font-family: 'IBM Plex Sans', sans-serif; color: var(--cyan);
font-size: 1.05rem; text-shadow: 0 0 12px var(--cyan-glow); font-size: 1.05rem; text-shadow: 0 0 12px var(--cyan-glow);
border: 1px solid var(--line-hi); padding: 0.1rem 0.34rem; border-radius: 3px; border: 1px solid var(--line-hi); padding: 0.1rem 0.34rem; border-radius: 3px;
background: rgba(53,224,255,0.06); background: rgba(53,224,255,0.06);
} }
.brand-name { .brand-name {
font-family: 'Orbitron', sans-serif; font-weight: 700; font-size: 0.95rem; font-family: 'IBM Plex Sans', sans-serif; font-weight: 700; font-size: 0.95rem;
color: #eafcff; letter-spacing: 0.14em; white-space: nowrap; color: #eafcff; letter-spacing: 0.14em; white-space: nowrap;
} }
.brand-name .accent { color: var(--cyan); text-shadow: 0 0 10px var(--cyan-glow); } .brand-name .accent { color: var(--cyan); text-shadow: 0 0 10px var(--cyan-glow); }
.brand-sub { .brand-sub {
font-family: 'Share Tech Mono', monospace; font-size: 0.58rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.58rem;
color: var(--muted); letter-spacing: 0.22em; text-transform: uppercase; color: var(--muted); letter-spacing: 0.22em; text-transform: uppercase;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
} }
@ -103,7 +103,7 @@
.topbar-right { display: flex; align-items: center; gap: 1rem; } .topbar-right { display: flex; align-items: center; gap: 1rem; }
.sys-status { .sys-status {
display: flex; align-items: center; gap: 0.5rem; display: flex; align-items: center; gap: 0.5rem;
font-family: 'Share Tech Mono', monospace; font-size: 0.72rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.72rem;
color: var(--muted); white-space: nowrap; color: var(--muted); white-space: nowrap;
} }
.sys-status .sep { opacity: 0.35; } .sys-status .sep { opacity: 0.35; }
@ -112,6 +112,10 @@
.dot.bad { background: var(--red); box-shadow: 0 0 8px var(--red); } .dot.bad { background: var(--red); box-shadow: 0 0 8px var(--red); }
.dot.dim { background: var(--muted); box-shadow: none; animation: none; } .dot.dim { background: var(--muted); box-shadow: none; animation: none; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
.clock, .lp-count, .hud-chip b, #utc-clock, #dvr-label {
font-variant-numeric: tabular-nums;
font-family: 'IBM Plex Mono', ui-monospace, monospace;
}
.clock { color: var(--cyan); } .clock { color: var(--cyan); }
@media (max-width: 720px) { .sys-status .hide-sm { display: none; } } @media (max-width: 720px) { .sys-status .hide-sm { display: none; } }
@ -121,7 +125,7 @@
display: flex; align-items: center; gap: 0.5rem; display: flex; align-items: center; gap: 0.5rem;
background: linear-gradient(180deg, var(--bg-3), var(--bg-2)); background: linear-gradient(180deg, var(--bg-3), var(--bg-2));
border: 1px solid var(--line-hi); color: var(--text); border: 1px solid var(--line-hi); color: var(--text);
font-family: 'Orbitron', sans-serif; font-size: 0.72rem; font-weight: 700; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.72rem; font-weight: 700;
letter-spacing: 0.16em; padding: 0.42rem 0.85rem; border-radius: 4px; letter-spacing: 0.16em; padding: 0.42rem 0.85rem; border-radius: 4px;
cursor: pointer; transition: border-color .15s, box-shadow .15s; cursor: pointer; transition: border-color .15s, box-shadow .15s;
} }
@ -143,7 +147,7 @@
.nav-item { .nav-item {
display: flex; align-items: center; gap: 0.6rem; width: 100%; display: flex; align-items: center; gap: 0.6rem; width: 100%;
background: none; border: none; color: var(--text); text-align: left; background: none; border: none; color: var(--text); text-align: left;
font-family: 'Rajdhani', sans-serif; font-weight: 600; font-size: 0.88rem; font-family: 'IBM Plex Sans', sans-serif; font-weight: 600; font-size: 0.88rem;
letter-spacing: 0.09em; text-transform: uppercase; letter-spacing: 0.09em; text-transform: uppercase;
padding: 0.55rem 0.7rem; border-radius: 4px; cursor: pointer; padding: 0.55rem 0.7rem; border-radius: 4px; cursor: pointer;
border-left: 2px solid transparent; border-left: 2px solid transparent;
@ -177,17 +181,12 @@
max-width: calc(100% - 300px); /* keep off LAYERS (left) and zoom (right) */ max-width: calc(100% - 300px); /* keep off LAYERS (left) and zoom (right) */
justify-content: center; justify-content: center;
} }
.chokepoint-btns { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; } .chokepoint-btns { display: none; }
.chokepoint-btns .btn { padding: 0.22rem 0.42rem; font-size: 0.66rem; letter-spacing: 0.04em; } #chokepoint-select { display: inline-block; min-width: 8rem; }
.chokepoint-btns .btn.is-on {
border-color: var(--cyan); color: var(--cyan);
box-shadow: 0 0 8px var(--cyan-glow); background: rgba(53,224,255,0.10);
}
#chokepoint-select { display: none; }
.map-toolbar select, .map-toolbar input[type="date"], .map-toolbar input[type="datetime-local"] { .map-toolbar select, .map-toolbar input[type="date"], .map-toolbar input[type="datetime-local"] {
background: var(--bg-1); color: var(--text); border: 1px solid var(--line); background: var(--bg-1); color: var(--text); border: 1px solid var(--line);
border-radius: 4px; padding: 0.3rem 0.45rem; font-size: 0.76rem; border-radius: 4px; padding: 0.3rem 0.45rem; font-size: 0.76rem;
font-family: 'Share Tech Mono', monospace; color-scheme: dark; font-family: 'IBM Plex Mono', monospace; color-scheme: dark;
} }
.map-toolbar select:focus, .map-toolbar input:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); } .map-toolbar select:focus, .map-toolbar input:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); }
.map-toolbar .btn { padding: 0.3rem 0.6rem; font-size: 0.72rem; } .map-toolbar .btn { padding: 0.3rem 0.6rem; font-size: 0.72rem; }
@ -206,7 +205,7 @@
.leaflet-control-zoom a:hover { background: var(--bg-3) !important; color: var(--cyan) !important; } .leaflet-control-zoom a:hover { background: var(--bg-3) !important; color: var(--cyan) !important; }
.leaflet-control-attribution { .leaflet-control-attribution {
background: rgba(3,6,9,0.78) !important; color: #8fa8bd !important; background: rgba(3,6,9,0.78) !important; color: #8fa8bd !important;
font-family: 'Share Tech Mono', monospace !important; font-size: 10px !important; font-family: 'IBM Plex Mono', monospace !important; font-size: 10px !important;
} }
.leaflet-control-attribution a { color: var(--cyan) !important; } .leaflet-control-attribution a { color: var(--cyan) !important; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip { .leaflet-popup-content-wrapper, .leaflet-popup-tip {
@ -264,7 +263,7 @@
/* ── Map hint + HUD chips ── */ /* ── Map hint + HUD chips ── */
.map-hint { .map-hint {
position: absolute; left: 12px; bottom: 12px; z-index: 500; position: absolute; left: 12px; bottom: 12px; z-index: 500;
font-family: 'Share Tech Mono', monospace; font-size: 0.68rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; color: var(--muted);
background: rgba(6,11,20,0.72); border: 1px solid var(--line); background: rgba(6,11,20,0.72); border: 1px solid var(--line);
border-radius: 4px; padding: 0.3rem 0.6rem; max-width: 60%; border-radius: 4px; padding: 0.3rem 0.6rem; max-width: 60%;
text-shadow: 0 0 6px rgba(0,0,0,0.8); text-shadow: 0 0 6px rgba(0,0,0,0.8);
@ -275,7 +274,7 @@
} }
.hud-chip { .hud-chip {
display: flex; align-items: center; gap: 0.45rem; display: flex; align-items: center; gap: 0.45rem;
font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--muted);
background: rgba(6,11,20,0.78); border: 1px solid var(--line); background: rgba(6,11,20,0.78); border: 1px solid var(--line);
border-radius: 4px; padding: 0.26rem 0.55rem; letter-spacing: 0.06em; border-radius: 4px; padding: 0.26rem 0.55rem; letter-spacing: 0.06em;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
@ -292,7 +291,7 @@
z-index: 520; display: flex; align-items: center; gap: 0.55rem; z-index: 520; display: flex; align-items: center; gap: 0.55rem;
background: rgba(6,11,20,0.86); border: 1px solid var(--line-hi); background: rgba(6,11,20,0.86); border: 1px solid var(--line-hi);
border-radius: 4px; padding: 0.28rem 0.7rem; border-radius: 4px; padding: 0.28rem 0.7rem;
font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--muted);
min-width: 280px; max-width: 52%; min-width: 280px; max-width: 52%;
} }
#dvr-bar input[type=range] { flex: 1; accent-color: var(--cyan); } #dvr-bar input[type=range] { flex: 1; accent-color: var(--cyan); }
@ -303,7 +302,7 @@
} }
#dvr-toast .gf-alert { #dvr-toast .gf-alert {
background: rgba(255,46,151,0.16); border: 1px solid var(--magenta); background: rgba(255,46,151,0.16); border: 1px solid var(--magenta);
color: var(--text); font-family: 'Share Tech Mono', monospace; color: var(--text); font-family: 'IBM Plex Mono', monospace;
font-size: 0.68rem; padding: 0.4rem 0.6rem; margin-bottom: 0.35rem; font-size: 0.68rem; padding: 0.4rem 0.6rem; margin-bottom: 0.35rem;
border-radius: 4px; border-radius: 4px;
} }
@ -311,17 +310,19 @@
/* ── Layer control panel (dark HUD) ── */ /* ── Layer control panel (dark HUD) ── */
#layer-panel { #layer-panel {
position: absolute; top: 58px; left: 12px; z-index: 600; position: absolute; top: 58px; left: 12px; z-index: 600;
width: 268px; max-height: calc(100% - 74px); overflow: hidden; width: min(320px, calc(100% - 24px)); max-width: 320px;
max-height: calc(100% - 74px); overflow: hidden;
background: rgba(6,11,20,0.86); backdrop-filter: blur(8px); background: rgba(6,11,20,0.86); backdrop-filter: blur(8px);
border: 1px solid var(--line-hi); border-radius: 6px; border: 1px solid var(--line-hi); border-radius: 6px;
box-shadow: 0 0 22px rgba(53,224,255,0.14), 0 0 2px rgba(53,224,255,0.4); box-shadow: 0 0 22px rgba(53,224,255,0.14), 0 0 2px rgba(53,224,255,0.4);
color: var(--text); font-size: 0.78rem; display: flex; flex-direction: column; color: var(--text); font-size: 0.78rem; display: flex; flex-direction: column;
clip-path: polygon(0 8px, 8px 0, calc(100% - 8px) 0, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px)); clip-path: polygon(0 8px, 8px 0, calc(100% - 8px) 0, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0 calc(100% - 8px));
} }
#layer-panel.collapsed { width: auto; max-width: 320px; }
.lp-head { .lp-head {
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
padding: 0.55rem 0.75rem; border-bottom: 1px solid var(--line); padding: 0.55rem 0.75rem; border-bottom: 1px solid var(--line);
font-family: 'Orbitron', sans-serif; font-size: 0.68rem; font-weight: 700; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.68rem; font-weight: 700;
letter-spacing: 0.14em; text-transform: uppercase; color: var(--cyan); letter-spacing: 0.14em; text-transform: uppercase; color: var(--cyan);
text-shadow: 0 0 10px rgba(53,224,255,0.55); cursor: pointer; user-select: none; text-shadow: 0 0 10px rgba(53,224,255,0.55); cursor: pointer; user-select: none;
background: linear-gradient(180deg, rgba(53,224,255,0.07), transparent); background: linear-gradient(180deg, rgba(53,224,255,0.07), transparent);
@ -341,7 +342,7 @@
.lp-dot.fires { background: #fb923c; box-shadow: 0 0 7px #fb923c; } .lp-dot.fires { background: #fb923c; box-shadow: 0 0 7px #fb923c; }
.lp-dot.cams { background: #4ade80; box-shadow: 0 0 7px #4ade80; } .lp-dot.cams { background: #4ade80; box-shadow: 0 0 7px #4ade80; }
.lp-dot.blips { background: var(--magenta); box-shadow: 0 0 7px var(--magenta); } .lp-dot.blips { background: var(--magenta); box-shadow: 0 0 7px var(--magenta); }
.lp-dot.news { background: var(--magenta); box-shadow: 0 0 7px var(--magenta); border-radius: 1px; transform: rotate(45deg); } .lp-dot.news { background: var(--cyan); box-shadow: 0 0 7px var(--cyan); }
.lp-dot.weather { background: var(--amber); box-shadow: 0 0 7px var(--amber); } .lp-dot.weather { background: var(--amber); box-shadow: 0 0 7px var(--amber); }
.lp-dot.flights { background: #7dd3fc; box-shadow: 0 0 7px #7dd3fc; } .lp-dot.flights { background: #7dd3fc; box-shadow: 0 0 7px #7dd3fc; }
.lp-dot.mil { background: #f472b6; box-shadow: 0 0 7px #f472b6; } .lp-dot.mil { background: #f472b6; box-shadow: 0 0 7px #f472b6; }
@ -357,15 +358,16 @@
box-shadow: 0 0 7px #64748b; box-shadow: 0 0 7px #64748b;
} }
.lp-dot.conflicts { background: #ff2a6d; box-shadow: 0 0 7px #ff2a6d; } .lp-dot.conflicts { background: #ff2a6d; box-shadow: 0 0 7px #ff2a6d; }
.lp-count { font-family: 'Share Tech Mono', monospace; font-size: 0.7rem; color: var(--cyan); } .lp-count { font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; color: var(--cyan); }
.lp-sub { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; } .lp-sub { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
.lp-opacity { display: flex; align-items: center; gap: 0.4rem; font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; } .lp-opacity { display: none; align-items: center; gap: 0.4rem; font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.lp-layer:has(.lp-name input[type="checkbox"]:checked) .lp-opacity { display: flex; }
.lp-opacity input[type="range"] { flex: 1; accent-color: var(--cyan); height: 4px; cursor: pointer; } .lp-opacity input[type="range"] { flex: 1; accent-color: var(--cyan); height: 4px; cursor: pointer; }
.lp-opacity b { color: var(--text); font-family: 'Share Tech Mono', monospace; font-weight: 400; min-width: 2.4em; text-align: right; } .lp-opacity b { color: var(--text); font-family: 'IBM Plex Mono', monospace; font-weight: 400; min-width: 2.4em; text-align: right; }
.lp-select { background: var(--bg-1); color: var(--text); border: 1px solid var(--line); border-radius: 4px; padding: 0.22rem 0.35rem; font-size: 0.68rem; font-family: 'Share Tech Mono', monospace; color-scheme: dark; } .lp-select { background: var(--bg-1); color: var(--text); border: 1px solid var(--line); border-radius: 4px; padding: 0.22rem 0.35rem; font-size: 0.68rem; font-family: 'IBM Plex Mono', monospace; color-scheme: dark; }
.lp-select:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); } .lp-select:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); }
.lp-legend { border-top: 1px solid var(--line); padding-top: 0.5rem; display: flex; flex-direction: column; gap: 0.35rem; } .lp-legend { border-top: 1px solid var(--line); padding-top: 0.5rem; display: flex; flex-direction: column; gap: 0.35rem; }
.lp-legend h4 { font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); margin: 0; font-family: 'Rajdhani', sans-serif; font-weight: 600; } .lp-legend h4 { font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); margin: 0; font-family: 'IBM Plex Sans', sans-serif; font-weight: 600; }
.lp-ramp { height: 8px; border-radius: 4px; background: linear-gradient(90deg, #2563eb, #38bdf8, #f59e0b, #ef4444, #fde047); box-shadow: 0 0 8px rgba(251,146,60,0.35) inset; } .lp-ramp { height: 8px; border-radius: 4px; background: linear-gradient(90deg, #2563eb, #38bdf8, #f59e0b, #ef4444, #fde047); box-shadow: 0 0 8px rgba(251,146,60,0.35) inset; }
.lp-ramp-scale { display: flex; justify-content: space-between; font-size: 0.58rem; color: var(--muted); } .lp-ramp-scale { display: flex; justify-content: space-between; font-size: 0.58rem; color: var(--muted); }
.lp-heat-legend { display: flex; align-items: center; justify-content: space-between; font-size: 0.64rem; color: var(--muted); gap: 0.5rem; } .lp-heat-legend { display: flex; align-items: center; justify-content: space-between; font-size: 0.64rem; color: var(--muted); gap: 0.5rem; }
@ -381,7 +383,7 @@
.gf-item .gf-name:hover { color: var(--cyan); } .gf-item .gf-name:hover { color: var(--cyan); }
.gf-item .gf-actions { display: flex; align-items: center; gap: 0.28rem; flex: 0 0 auto; } .gf-item .gf-actions { display: flex; align-items: center; gap: 0.28rem; flex: 0 0 auto; }
.gf-item input.gf-active { accent-color: var(--cyan); cursor: pointer; width: 14px; height: 14px; } .gf-item input.gf-active { accent-color: var(--cyan); cursor: pointer; width: 14px; height: 14px; }
.gf-del, .gf-go, .gf-ren { background: transparent; border: 1px solid var(--magenta); color: var(--magenta); font-family: 'Share Tech Mono', monospace; font-size: 0.58rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.12rem 0.35rem; border-radius: 3px; cursor: pointer; } .gf-del, .gf-go, .gf-ren { background: transparent; border: 1px solid var(--magenta); color: var(--magenta); font-family: 'IBM Plex Mono', monospace; font-size: 0.58rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.12rem 0.35rem; border-radius: 3px; cursor: pointer; }
.gf-go, .gf-ren { border-color: var(--cyan); color: var(--cyan); } .gf-go, .gf-ren { border-color: var(--cyan); color: var(--cyan); }
.gf-del:hover { background: rgba(255,46,151,0.16); } .gf-del:hover { background: rgba(255,46,151,0.16); }
.gf-go:hover, .gf-ren:hover { background: rgba(53,224,255,0.14); } .gf-go:hover, .gf-ren:hover { background: rgba(53,224,255,0.14); }
@ -398,7 +400,7 @@
.gf-hit { .gf-hit {
cursor: pointer; font-size: 0.58rem; line-height: 1.35; color: var(--text); cursor: pointer; font-size: 0.58rem; line-height: 1.35; color: var(--text);
padding: 0.22rem 0.28rem; border: 1px solid var(--line); border-radius: 3px; padding: 0.22rem 0.28rem; border: 1px solid var(--line); border-radius: 3px;
font-family: 'Share Tech Mono', monospace; font-family: 'IBM Plex Mono', monospace;
} }
.gf-hit:hover, .gf-hit:focus { border-color: var(--cyan); outline: none; background: rgba(53,224,255,0.08); } .gf-hit:hover, .gf-hit:focus { border-color: var(--cyan); outline: none; background: rgba(53,224,255,0.08); }
.gf-hit .gf-hit-meta { color: var(--muted); } .gf-hit .gf-hit-meta { color: var(--muted); }
@ -419,7 +421,7 @@
} }
.cheat-sheet[hidden] { display: none !important; } .cheat-sheet[hidden] { display: none !important; }
.cheat-sheet h3 { .cheat-sheet h3 {
font-family: 'Orbitron', sans-serif; font-size: 0.72rem; letter-spacing: 0.16em; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.72rem; letter-spacing: 0.16em;
color: var(--cyan); margin: 0 0 0.65rem; text-transform: uppercase; color: var(--cyan); margin: 0 0 0.65rem; text-transform: uppercase;
} }
.cheat-sheet dl { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 0.85rem; margin: 0; } .cheat-sheet dl { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 0.85rem; margin: 0; }
@ -445,26 +447,26 @@
.pd-head { .pd-head {
display: flex; justify-content: space-between; align-items: center; display: flex; justify-content: space-between; align-items: center;
padding: 0.55rem 0.95rem 0.55rem 0.85rem; border-bottom: 1px solid var(--line); padding: 0.55rem 0.95rem 0.55rem 0.85rem; border-bottom: 1px solid var(--line);
font-family: 'Orbitron', sans-serif; font-size: 0.68rem; font-weight: 700; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.68rem; font-weight: 700;
letter-spacing: 0.14em; text-transform: uppercase; color: var(--cyan); letter-spacing: 0.14em; text-transform: uppercase; color: var(--cyan);
text-shadow: 0 0 10px rgba(53,224,255,0.55); text-shadow: 0 0 10px rgba(53,224,255,0.55);
background: linear-gradient(180deg, rgba(53,224,255,0.07), transparent); background: linear-gradient(180deg, rgba(53,224,255,0.07), transparent);
} }
.pd-close { .pd-close {
background: transparent; border: 1px solid var(--line); color: var(--muted); background: transparent; border: 1px solid var(--line); color: var(--muted);
font-family: 'Share Tech Mono', monospace; font-size: 0.78rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.78rem;
width: 28px; height: 28px; border-radius: 4px; cursor: pointer; line-height: 1; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; line-height: 1;
flex-shrink: 0; margin-right: 2px; flex-shrink: 0; margin-right: 2px;
} }
.pd-close:hover { border-color: var(--cyan); color: var(--cyan); } .pd-close:hover { border-color: var(--cyan); color: var(--cyan); }
.pd-body { padding: 0.55rem 0.75rem 0.7rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.55rem; min-height: 0; flex: 1; } .pd-body { padding: 0.55rem 0.75rem 0.7rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.55rem; min-height: 0; flex: 1; }
.pd-status { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); } .pd-status { font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--muted); }
.pd-status.err { color: var(--red); } .pd-status.err { color: var(--red); }
.pd-coords { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--cyan); letter-spacing: 0.04em; } .pd-coords { font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--cyan); letter-spacing: 0.04em; }
.pd-name { font-family: 'Rajdhani', sans-serif; font-weight: 700; font-size: 0.92rem; color: var(--text); line-height: 1.25; } .pd-name { font-family: 'IBM Plex Sans', sans-serif; font-weight: 700; font-size: 0.92rem; color: var(--text); line-height: 1.25; }
.pd-addr { font-size: 0.72rem; color: var(--muted); line-height: 1.4; } .pd-addr { font-size: 0.72rem; color: var(--muted); line-height: 1.4; }
.pd-sec { .pd-sec {
font-family: 'Orbitron', sans-serif; font-size: 0.58rem; letter-spacing: 0.12em; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.58rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--muted); margin-top: 0.15rem; text-transform: uppercase; color: var(--muted); margin-top: 0.15rem;
} }
.pd-list { display: flex; flex-direction: column; gap: 0.18rem; } .pd-list { display: flex; flex-direction: column; gap: 0.18rem; }
@ -472,11 +474,11 @@
display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem;
background: transparent; border: 0; border-left: 2px solid var(--line); background: transparent; border: 0; border-left: 2px solid var(--line);
color: var(--text); text-align: left; cursor: pointer; padding: 0.22rem 0.35rem; color: var(--text); text-align: left; cursor: pointer; padding: 0.22rem 0.35rem;
font-family: 'Rajdhani', sans-serif; font-size: 0.78rem; font-weight: 600; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.78rem; font-weight: 600;
} }
.pd-item:hover, .pd-item:focus-visible { border-left-color: var(--cyan); color: var(--cyan); background: rgba(53,224,255,0.06); } .pd-item:hover, .pd-item:focus-visible { border-left-color: var(--cyan); color: var(--cyan); background: rgba(53,224,255,0.06); }
.pd-item .k { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.58rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; flex-shrink: 0; } .pd-item .k { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.58rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; flex-shrink: 0; }
.pd-item .d { font-family: 'Share Tech Mono', monospace; font-size: 0.62rem; color: var(--cyan); white-space: nowrap; } .pd-item .d { font-family: 'IBM Plex Mono', monospace; font-size: 0.62rem; color: var(--cyan); white-space: nowrap; }
.pd-empty { font-size: 0.72rem; color: var(--muted); font-style: italic; } .pd-empty { font-size: 0.72rem; color: var(--muted); font-style: italic; }
.pd-note { font-size: 0.6rem; color: var(--muted); opacity: 0.85; line-height: 1.35; } .pd-note { font-size: 0.6rem; color: var(--muted); opacity: 0.85; line-height: 1.35; }
@ -487,7 +489,7 @@
.cam-pop .thumb.placeholder { display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 0.7rem; height: 80px; } .cam-pop .thumb.placeholder { display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 0.7rem; height: 80px; }
.cam-pop table { width: 100%; font-size: 0.72rem; border-collapse: collapse; } .cam-pop table { width: 100%; font-size: 0.72rem; border-collapse: collapse; }
.cam-pop td { padding: 0.12rem 0.2rem; vertical-align: top; } .cam-pop td { padding: 0.12rem 0.2rem; vertical-align: top; }
.cam-pop td.k { color: var(--muted); text-transform: uppercase; font-size: 0.6rem; letter-spacing: 0.05em; white-space: nowrap; width: 34%; font-family: 'Share Tech Mono', monospace; } .cam-pop td.k { color: var(--muted); text-transform: uppercase; font-size: 0.6rem; letter-spacing: 0.05em; white-space: nowrap; width: 34%; font-family: 'IBM Plex Mono', monospace; }
.cam-pop a { color: var(--cyan); word-break: break-all; } .cam-pop a { color: var(--cyan); word-break: break-all; }
.cam-pop .ps-photo { margin-top: 0.45rem; } .cam-pop .ps-photo { margin-top: 0.45rem; }
.cam-pop .ps-photo a.ps-link { display: block; } .cam-pop .ps-photo a.ps-link { display: block; }
@ -500,7 +502,7 @@
.cam-pop .ps-credit a { color: var(--cyan); } .cam-pop .ps-credit a { color: var(--cyan); }
.cam-pop .ps-loading { font-size: 0.68rem; color: var(--muted); margin-top: 0.35rem; } .cam-pop .ps-loading { font-size: 0.68rem; color: var(--muted); margin-top: 0.35rem; }
.role-badge { .role-badge {
display: inline-block; font-family: 'Share Tech Mono', monospace; display: inline-block; font-family: 'IBM Plex Mono', monospace;
font-size: 0.58rem; font-weight: 700; letter-spacing: 0.12em; font-size: 0.58rem; font-weight: 700; letter-spacing: 0.12em;
padding: 0.08rem 0.4rem; border-radius: 3px; margin-left: 0.4rem; padding: 0.08rem 0.4rem; border-radius: 3px; margin-left: 0.4rem;
vertical-align: middle; vertical-align: middle;
@ -514,8 +516,8 @@
animation: emerg-pulse 1.4s ease-in-out infinite; animation: emerg-pulse 1.4s ease-in-out infinite;
} }
.blip-pop { min-width: 200px; max-width: 260px; } .blip-pop { min-width: 200px; max-width: 260px; }
.blip-pop .blip-src { font-family: 'Share Tech Mono', monospace; font-size: 0.62rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.08em; } .blip-pop .blip-src { font-family: 'IBM Plex Mono', monospace; font-size: 0.62rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.08em; }
.blip-pop .blip-time { font-size: 0.7rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; margin: 0.2rem 0 0.3rem; } .blip-pop .blip-time { font-size: 0.7rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; margin: 0.2rem 0 0.3rem; }
.blip-pop .blip-desc { font-size: 0.72rem; color: var(--text); line-height: 1.35; margin-top: 0.15rem; } .blip-pop .blip-desc { font-size: 0.72rem; color: var(--text); line-height: 1.35; margin-top: 0.15rem; }
/* ── Sub-views (News / Events / Alerts / ... ) ── */ /* ── Sub-views (News / Events / Alerts / ... ) ── */
@ -524,7 +526,7 @@
max-width: 1280px; margin: 0 auto; max-width: 1280px; margin: 0 auto;
} }
.subview > h2 { .subview > h2 {
font-family: 'Orbitron', sans-serif; font-size: 0.95rem; font-weight: 700; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.95rem; font-weight: 700;
letter-spacing: 0.16em; text-transform: uppercase; color: var(--text); letter-spacing: 0.16em; text-transform: uppercase; color: var(--text);
display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.35rem; display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.35rem;
} }
@ -536,13 +538,13 @@
box-shadow: 0 0 18px rgba(53,224,255,0.05); box-shadow: 0 0 18px rgba(53,224,255,0.05);
} }
.panel h3 { .panel h3 {
font-size: 0.68rem; font-family: 'Orbitron', sans-serif; font-weight: 700; font-size: 0.68rem; font-family: 'IBM Plex Sans', sans-serif; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.16em; color: var(--muted); text-transform: uppercase; letter-spacing: 0.16em; color: var(--muted);
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
} }
table { width: 100%; border-collapse: collapse; font-size: 0.86rem; } table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
th, td { text-align: left; padding: 0.5rem 0.65rem; border-bottom: 1px solid var(--line); } th, td { text-align: left; padding: 0.5rem 0.65rem; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 600; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.1em; font-family: 'Rajdhani', sans-serif; } th { color: var(--muted); font-weight: 600; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.1em; font-family: 'IBM Plex Sans', sans-serif; }
tr:hover td { background: rgba(53,224,255,0.04); } tr:hover td { background: rgba(53,224,255,0.04); }
.badge { display: inline-block; padding: 0.12rem 0.5rem; border-radius: 3px; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; } .badge { display: inline-block; padding: 0.12rem 0.5rem; border-radius: 3px; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; }
.badge-positive { background: rgba(83,240,165,0.12); color: var(--green); border: 1px solid rgba(83,240,165,0.35); } .badge-positive { background: rgba(83,240,165,0.12); color: var(--green); border: 1px solid rgba(83,240,165,0.35); }
@ -555,7 +557,7 @@
/* ── Stats strip (Events view) ── */ /* ── Stats strip (Events view) ── */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.8rem; margin-bottom: 1.1rem; } .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.8rem; margin-bottom: 1.1rem; }
.stat-card { position: relative; overflow: hidden; } .stat-card { position: relative; overflow: hidden; }
.stat-card .value { font-family: 'Orbitron', sans-serif; font-size: 1.5rem; font-weight: 700; color: var(--text); letter-spacing: 0.04em; } .stat-card .value { font-family: 'IBM Plex Sans', sans-serif; font-size: 1.5rem; font-weight: 700; color: var(--text); letter-spacing: 0.04em; }
.stat-card .value.cyan { color: var(--cyan); text-shadow: 0 0 14px var(--cyan-glow); } .stat-card .value.cyan { color: var(--cyan); text-shadow: 0 0 14px var(--cyan-glow); }
.stat-card .value.red { color: var(--red); text-shadow: 0 0 14px rgba(255,93,93,0.4); } .stat-card .value.red { color: var(--red); text-shadow: 0 0 14px rgba(255,93,93,0.4); }
.stat-card::after { .stat-card::after {
@ -568,14 +570,14 @@
.search-box input { .search-box input {
flex: 1; background: var(--bg-1); border: 1px solid var(--line); flex: 1; background: var(--bg-1); border: 1px solid var(--line);
border-radius: 4px; padding: 0.55rem 0.9rem; color: var(--text); border-radius: 4px; padding: 0.55rem 0.9rem; color: var(--text);
font-size: 0.88rem; font-family: 'Rajdhani', sans-serif; font-size: 0.88rem; font-family: 'IBM Plex Sans', sans-serif;
} }
.search-box input:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 10px var(--cyan-glow); } .search-box input:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 10px var(--cyan-glow); }
/* ── News view ── */ /* ── News view ── */
.news-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 0.9rem; flex-wrap: wrap; } .news-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 0.9rem; flex-wrap: wrap; }
.news-refresh { display: flex; align-items: center; gap: 0.6rem; } .news-refresh { display: flex; align-items: center; gap: 0.6rem; }
.news-updated { font-size: 0.7rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; } .news-updated { font-size: 0.7rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; }
.news-summary { .news-summary {
position: relative; background: linear-gradient(160deg, rgba(53,224,255,0.10), rgba(10,19,34,0.7) 45%); position: relative; background: linear-gradient(160deg, rgba(53,224,255,0.10), rgba(10,19,34,0.7) 45%);
border: 1px solid rgba(53,224,255,0.35); border-radius: 8px; padding: 1rem 1.25rem 1.1rem; border: 1px solid rgba(53,224,255,0.35); border-radius: 8px; padding: 1rem 1.25rem 1.1rem;
@ -586,19 +588,19 @@
content: ''; position: absolute; inset: 0; pointer-events: none; content: ''; position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(140px 70px at 85% -10%, rgba(53,224,255,0.18), transparent 70%); background: radial-gradient(140px 70px at 85% -10%, rgba(53,224,255,0.18), transparent 70%);
} }
.ns-kicker { font-family: 'Orbitron', sans-serif; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--cyan); text-shadow: 0 0 12px rgba(53,224,255,0.6); display: flex; align-items: center; gap: 0.5rem; } .ns-kicker { font-family: 'IBM Plex Sans', sans-serif; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--cyan); text-shadow: 0 0 12px rgba(53,224,255,0.6); display: flex; align-items: center; gap: 0.5rem; }
.ns-badge { font-size: 0.58rem; font-weight: 700; background: rgba(53,224,255,0.14); border: 1px solid rgba(53,224,255,0.4); border-radius: 3px; padding: 0.1rem 0.45rem; color: var(--cyan); letter-spacing: 0.1em; font-family: 'Share Tech Mono', monospace; } .ns-badge { font-size: 0.58rem; font-weight: 700; background: rgba(53,224,255,0.14); border: 1px solid rgba(53,224,255,0.4); border-radius: 3px; padding: 0.1rem 0.45rem; color: var(--cyan); letter-spacing: 0.1em; font-family: 'IBM Plex Mono', monospace; }
.ns-time { font-size: 0.68rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; margin: 0.35rem 0 0.6rem; } .ns-time { font-size: 0.68rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; margin: 0.35rem 0 0.6rem; }
.ns-body { font-size: 0.9rem; line-height: 1.62; color: var(--text); white-space: pre-wrap; position: relative; z-index: 1; } .ns-body { font-size: 0.9rem; line-height: 1.62; color: var(--text); white-space: pre-wrap; position: relative; z-index: 1; }
.ns-body h1, .ns-body h2, .ns-body h3, .ns-body h4 { font-size: 0.98rem; margin: 0.5rem 0 0.25rem; color: var(--cyan); font-family: 'Rajdhani', sans-serif; } .ns-body h1, .ns-body h2, .ns-body h3, .ns-body h4 { font-size: 0.98rem; margin: 0.5rem 0 0.25rem; color: var(--cyan); font-family: 'IBM Plex Sans', sans-serif; }
.ns-body ul, .ns-body ol { margin: 0.35rem 0 0.35rem 1.2rem; } .ns-body ul, .ns-body ol { margin: 0.35rem 0 0.35rem 1.2rem; }
.ns-body li { margin-bottom: 0.2rem; } .ns-body li { margin-bottom: 0.2rem; }
.ns-body code { background: rgba(3,6,9,0.6); border: 1px solid var(--line); border-radius: 3px; padding: 0.05rem 0.3rem; font-size: 0.85em; font-family: 'Share Tech Mono', monospace; } .ns-body code { background: rgba(3,6,9,0.6); border: 1px solid var(--line); border-radius: 3px; padding: 0.05rem 0.3rem; font-size: 0.85em; font-family: 'IBM Plex Mono', monospace; }
.ns-empty { color: var(--muted); font-size: 0.85rem; font-style: italic; } .ns-empty { color: var(--muted); font-size: 0.85rem; font-style: italic; }
.ns-loading { color: var(--muted); font-size: 0.85rem; } .ns-loading { color: var(--muted); font-size: 0.85rem; }
.ns-error { color: var(--red); font-size: 0.85rem; } .ns-error { color: var(--red); font-size: 0.85rem; }
.news-list-head { display: flex; justify-content: space-between; align-items: center; font-family: 'Orbitron', sans-serif; font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.4rem; } .news-list-head { display: flex; justify-content: space-between; align-items: center; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.4rem; }
.news-count { font-size: 0.66rem; color: var(--cyan); font-family: 'Share Tech Mono', monospace; } .news-count { font-size: 0.66rem; color: var(--cyan); font-family: 'IBM Plex Mono', monospace; }
.news-list { max-height: 52vh; min-height: 120px; overflow-y: auto; border: 1px solid var(--line); border-radius: 6px; background: rgba(6,11,20,0.4); } .news-list { max-height: 52vh; min-height: 120px; overflow-y: auto; border: 1px solid var(--line); border-radius: 6px; background: rgba(6,11,20,0.4); }
.news-list::-webkit-scrollbar { width: 8px; } .news-list::-webkit-scrollbar { width: 8px; }
.news-list::-webkit-scrollbar-track { background: rgba(14,26,46,0.4); border-radius: 8px; } .news-list::-webkit-scrollbar-track { background: rgba(14,26,46,0.4); border-radius: 8px; }
@ -607,10 +609,10 @@
.news-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.55rem 0.9rem; border-bottom: 1px solid rgba(20,50,74,0.5); text-decoration: none; color: var(--text); transition: background .15s, box-shadow .15s; } .news-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.55rem 0.9rem; border-bottom: 1px solid rgba(20,50,74,0.5); text-decoration: none; color: var(--text); transition: background .15s, box-shadow .15s; }
.news-item:last-child { border-bottom: none; } .news-item:last-child { border-bottom: none; }
.news-item:hover { background: rgba(53,224,255,0.06); box-shadow: inset 2px 0 0 var(--cyan); } .news-item:hover { background: rgba(53,224,255,0.06); box-shadow: inset 2px 0 0 var(--cyan); }
.news-dom { font-size: 0.62rem; font-family: 'Share Tech Mono', monospace; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.25); border-radius: 3px; padding: 0.1rem 0.45rem; white-space: nowrap; flex-shrink: 0; max-width: 30%; overflow: hidden; text-overflow: ellipsis; } .news-dom { font-size: 0.62rem; font-family: 'IBM Plex Mono', monospace; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.25); border-radius: 3px; padding: 0.1rem 0.45rem; white-space: nowrap; flex-shrink: 0; max-width: 30%; overflow: hidden; text-overflow: ellipsis; }
.news-title { flex: 1; font-size: 0.86rem; line-height: 1.35; color: var(--text); word-break: break-word; } .news-title { flex: 1; font-size: 0.86rem; line-height: 1.35; color: var(--text); word-break: break-word; }
.news-item:hover .news-title { color: var(--cyan); } .news-item:hover .news-title { color: var(--cyan); }
.news-time { font-size: 0.68rem; color: var(--muted); font-family: 'Share Tech Mono', monospace; white-space: nowrap; flex-shrink: 0; } .news-time { font-size: 0.68rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; white-space: nowrap; flex-shrink: 0; }
.news-geo { color: var(--green); font-size: 0.8rem; flex-shrink: 0; text-shadow: 0 0 8px rgba(83,240,165,0.6); } .news-geo { color: var(--green); font-size: 0.8rem; flex-shrink: 0; text-shadow: 0 0 8px rgba(83,240,165,0.6); }
.news-loading, .news-error { padding: 1rem 0.9rem; color: var(--muted); font-size: 0.85rem; } .news-loading, .news-error { padding: 1rem 0.9rem; color: var(--muted); font-size: 0.85rem; }
.news-error { color: var(--red); } .news-error { color: var(--red); }
@ -622,14 +624,14 @@
.key-card:hover { box-shadow: 0 0 22px rgba(53,224,255,0.12); } .key-card:hover { box-shadow: 0 0 22px rgba(53,224,255,0.12); }
.key-card.set { border-color: rgba(83,240,165,0.45); box-shadow: 0 0 16px rgba(83,240,165,0.12); } .key-card.set { border-color: rgba(83,240,165,0.45); box-shadow: 0 0 16px rgba(83,240,165,0.12); }
.key-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; } .key-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; }
.key-name { font-family: 'Share Tech Mono', monospace; font-size: 0.86rem; font-weight: 600; color: var(--cyan); word-break: break-all; } .key-name { font-family: 'IBM Plex Mono', monospace; font-size: 0.86rem; font-weight: 600; color: var(--cyan); word-break: break-all; }
.key-status { font-size: 0.68rem; font-family: 'Share Tech Mono', monospace; padding: 0.18rem 0.55rem; border-radius: 3px; white-space: nowrap; } .key-status { font-size: 0.68rem; font-family: 'IBM Plex Mono', monospace; padding: 0.18rem 0.55rem; border-radius: 3px; white-space: nowrap; }
.key-status.set { background: rgba(83,240,165,0.1); color: var(--green); box-shadow: 0 0 10px rgba(83,240,165,0.3); } .key-status.set { background: rgba(83,240,165,0.1); color: var(--green); box-shadow: 0 0 10px rgba(83,240,165,0.3); }
.key-status.missing { background: rgba(255,93,93,0.1); color: var(--red); box-shadow: 0 0 10px rgba(255,93,93,0.2); } .key-status.missing { background: rgba(255,93,93,0.1); color: var(--red); box-shadow: 0 0 10px rgba(255,93,93,0.2); }
.key-desc { font-size: 0.8rem; color: var(--muted); margin-bottom: 0.7rem; overflow-wrap: anywhere; } .key-desc { font-size: 0.8rem; color: var(--muted); margin-bottom: 0.7rem; overflow-wrap: anywhere; }
.key-hint { font-size: 0.7rem; color: var(--muted); opacity: 0.8; margin-top: 0.15rem; font-family: 'Share Tech Mono', monospace; } .key-hint { font-size: 0.7rem; color: var(--muted); opacity: 0.8; margin-top: 0.15rem; font-family: 'IBM Plex Mono', monospace; }
.key-form { display: flex; gap: 0.5rem; } .key-form { display: flex; gap: 0.5rem; }
.key-form input { flex: 1; background: var(--bg-0); border: 1px solid var(--line); border-radius: 4px; padding: 0.5rem 0.6rem; color: var(--text); font-size: 0.82rem; font-family: 'Share Tech Mono', monospace; } .key-form input { flex: 1; background: var(--bg-0); border: 1px solid var(--line); border-radius: 4px; padding: 0.5rem 0.6rem; color: var(--text); font-size: 0.82rem; font-family: 'IBM Plex Mono', monospace; }
.key-form input:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); } .key-form input:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); }
.key-form .btn { white-space: nowrap; } .key-form .btn { white-space: nowrap; }
.key-form .btn-danger { border-color: rgba(255,93,93,0.5); color: var(--red); } .key-form .btn-danger { border-color: rgba(255,93,93,0.5); color: var(--red); }
@ -645,17 +647,17 @@
.set-row textarea, .set-row input[type="text"], .set-row select { .set-row textarea, .set-row input[type="text"], .set-row select {
background: var(--bg-0); color: var(--text); border: 1px solid var(--line); background: var(--bg-0); color: var(--text); border: 1px solid var(--line);
border-radius: 4px; padding: 0.5rem 0.6rem; font-size: 0.85rem; border-radius: 4px; padding: 0.5rem 0.6rem; font-size: 0.85rem;
font-family: 'Share Tech Mono', monospace; color-scheme: dark; font-family: 'IBM Plex Mono', monospace; color-scheme: dark;
} }
.set-row textarea:focus, .set-row input:focus, .set-row select:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); } .set-row textarea:focus, .set-row input:focus, .set-row select:focus { outline: none; border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); }
.set-note { font-size: 0.72rem; color: var(--muted); line-height: 1.5; } .set-note { font-size: 0.72rem; color: var(--muted); line-height: 1.5; }
.set-note code { background: rgba(3,6,9,0.7); border: 1px solid var(--line); border-radius: 3px; padding: 0.05rem 0.35rem; font-size: 0.82em; } .set-note code { background: rgba(3,6,9,0.7); border: 1px solid var(--line); border-radius: 3px; padding: 0.05rem 0.35rem; font-size: 0.82em; }
.set-actions { display: flex; gap: 0.6rem; margin-top: 0.4rem; } .set-actions { display: flex; gap: 0.6rem; margin-top: 0.4rem; }
.set-saved { color: var(--green); font-size: 0.75rem; font-family: 'Share Tech Mono', monospace; min-height: 1.1rem; } .set-saved { color: var(--green); font-size: 0.75rem; font-family: 'IBM Plex Mono', monospace; min-height: 1.1rem; }
.sys-row { display: flex; justify-content: space-between; padding: 0.32rem 0; border-bottom: 1px dashed var(--line); font-size: 0.8rem; } .sys-row { display: flex; justify-content: space-between; padding: 0.32rem 0; border-bottom: 1px dashed var(--line); font-size: 0.8rem; }
.sys-row:last-child { border-bottom: none; } .sys-row:last-child { border-bottom: none; }
.sys-row .k { color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem; } .sys-row .k { color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem; }
.sys-row .v { font-family: 'Share Tech Mono', monospace; } .sys-row .v { font-family: 'IBM Plex Mono', monospace; }
/* ═══════════════ BOTTOM DOCK: tickers ═══════════════ */ /* ═══════════════ BOTTOM DOCK: tickers ═══════════════ */
.dock { .dock {
@ -672,12 +674,15 @@
.tick-label { .tick-label {
display: flex; align-items: center; justify-content: center; gap: 0.4rem; display: flex; align-items: center; justify-content: center; gap: 0.4rem;
min-width: 74px; padding: 0 0.7rem; flex-shrink: 0; min-width: 74px; padding: 0 0.7rem; flex-shrink: 0;
font-family: 'Orbitron', sans-serif; font-size: 0.62rem; font-weight: 700; font-family: 'IBM Plex Sans', sans-serif; font-size: 0.62rem; font-weight: 700;
letter-spacing: 0.2em; color: #02131c; background: var(--cyan); letter-spacing: 0.2em; color: #02131c; background: var(--cyan);
text-shadow: none; position: relative; text-shadow: none; position: relative;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 100%, 0 100%); clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 100%, 0 100%);
} }
.ticker.news .tick-label { background: var(--magenta); color: #1c0311; } .ticker.news .tick-label { background: var(--cyan); color: #02131c; }
.ticker.market[hidden] { display: none !important; }
.dock.news-only { height: 32px; }
.dock.news-only .ticker { height: 100%; }
.tick-label .st { animation: pulse 1.6s infinite; } .tick-label .st { animation: pulse 1.6s infinite; }
.tick-wrap { flex: 1; overflow: hidden; position: relative; display: flex; align-items: center; .tick-wrap { flex: 1; overflow: hidden; position: relative; display: flex; align-items: center;
mask-image: linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%); mask-image: linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%);
@ -693,20 +698,20 @@
font-size: 0.82rem; padding: 0 1rem; color: var(--text); text-decoration: none; font-size: 0.82rem; padding: 0 1rem; color: var(--text); text-decoration: none;
font-weight: 500; font-weight: 500;
} }
.tick-item .sym { font-family: 'Share Tech Mono', monospace; color: var(--cyan); font-size: 0.74rem; } .tick-item .sym { font-family: 'IBM Plex Mono', monospace; color: var(--cyan); font-size: 0.74rem; }
.tick-item .price { font-family: 'Share Tech Mono', monospace; color: var(--text); } .tick-item .price { font-family: 'IBM Plex Mono', monospace; color: var(--text); }
.tick-item .chg.up { color: var(--green); } .tick-item .chg.up { color: var(--green); }
.tick-item .chg.down { color: var(--red); } .tick-item .chg.down { color: var(--red); }
.tick-item .chg.flat { color: var(--muted); } .tick-item .chg.flat { color: var(--muted); }
.tick-item .dom { font-family: 'Share Tech Mono', monospace; font-size: 0.64rem; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.22); border-radius: 3px; padding: 0.05rem 0.4rem; } .tick-item .dom { font-family: 'IBM Plex Mono', monospace; font-size: 0.64rem; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.22); border-radius: 3px; padding: 0.05rem 0.4rem; }
.tick-item .tt { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); } .tick-item .tt { font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--muted); }
.tick-item .sep { color: var(--line-hi); } .tick-item .sep { color: var(--line-hi); }
button.tick-item { cursor: pointer; background: transparent; border: 0; font: inherit; color: inherit; } button.tick-item { cursor: pointer; background: transparent; border: 0; font: inherit; color: inherit; }
.tick-item.brief { .tick-item.brief {
cursor: pointer; background: linear-gradient(90deg, rgba(255,46,151,0.12), transparent 70%); cursor: pointer; background: linear-gradient(90deg, rgba(255,46,151,0.12), transparent 70%);
border-left: 2px solid var(--magenta); font-weight: 600; border-left: 2px solid var(--magenta); font-weight: 600;
} }
.tick-item .b-tag { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: #1c0311; background: var(--magenta); border-radius: 2px; padding: 0.08rem 0.4rem; letter-spacing: 0.1em; } .tick-item .b-tag { font-family: 'IBM Plex Mono', monospace; font-size: 0.6rem; color: #1c0311; background: var(--magenta); border-radius: 2px; padding: 0.08rem 0.4rem; letter-spacing: 0.1em; }
.tick-item .b-tag.critical { background: var(--red); } .tick-item .b-tag.critical { background: var(--red); }
.tick-item .b-tag.high { background: var(--amber); } .tick-item .b-tag.high { background: var(--amber); }
.tick-item .b-tag.medium { background: var(--cyan); color: #041018; } .tick-item .b-tag.medium { background: var(--cyan); color: #041018; }
@ -717,7 +722,7 @@
.tick-sep { color: var(--line-hi); } .tick-sep { color: var(--line-hi); }
.tick-tag { .tick-tag {
display: flex; align-items: center; flex-shrink: 0; padding: 0 0.8rem; display: flex; align-items: center; flex-shrink: 0; padding: 0 0.8rem;
font-family: 'Share Tech Mono', monospace; font-size: 0.62rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.62rem;
letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
border-left: 1px solid var(--line); border-left: 1px solid var(--line);
} }
@ -733,16 +738,16 @@
} }
.boot.gone { opacity: 0; pointer-events: none; } .boot.gone { opacity: 0; pointer-events: none; }
.boot-inner { text-align: center; } .boot-inner { text-align: center; }
.boot-mark { font-family: 'Orbitron', sans-serif; font-size: 2.2rem; color: var(--cyan); text-shadow: 0 0 24px var(--cyan-glow); margin-bottom: 0.6rem; } .boot-mark { font-family: 'IBM Plex Sans', sans-serif; font-size: 2.2rem; color: var(--cyan); text-shadow: 0 0 24px var(--cyan-glow); margin-bottom: 0.6rem; }
.boot-title { font-family: 'Orbitron', sans-serif; font-size: 1.3rem; font-weight: 900; letter-spacing: 0.3em; color: #eafcff; } .boot-title { font-family: 'IBM Plex Sans', sans-serif; font-size: 1.3rem; font-weight: 900; letter-spacing: 0.3em; color: #eafcff; }
.boot-title span { color: var(--cyan); } .boot-title span { color: var(--cyan); }
.boot-sub { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); letter-spacing: 0.3em; margin-top: 0.5rem; } .boot-sub { font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--muted); letter-spacing: 0.3em; margin-top: 0.5rem; }
.boot-bar { width: 220px; height: 3px; margin: 1.1rem auto 0; background: var(--bg-2); border-radius: 2px; overflow: hidden; } .boot-bar { width: 220px; height: 3px; margin: 1.1rem auto 0; background: var(--bg-2); border-radius: 2px; overflow: hidden; }
.boot-bar i { display: block; height: 100%; width: 40%; background: linear-gradient(90deg, transparent, var(--cyan), transparent); animation: bootSweep 0.9s linear infinite; } .boot-bar i { display: block; height: 100%; width: 40%; background: linear-gradient(90deg, transparent, var(--cyan), transparent); animation: bootSweep 0.9s linear infinite; }
@keyframes bootSweep { from { transform: translateX(-100%); } to { transform: translateX(350%); } } @keyframes bootSweep { from { transform: translateX(-100%); } to { transform: translateX(350%); } }
@media (max-width: 820px) { @media (max-width: 820px) {
:root { --topbar-h: 42px; --dock-h: 56px; } :root { --topbar-h: 42px; --dock-h: 32px; }
.tick-label { min-width: 56px; padding: 0 0.5rem; } .tick-label { min-width: 56px; padding: 0 0.5rem; }
.tick-tag { display: none; } .tick-tag { display: none; }
.hud-chips { display: none; } .hud-chips { display: none; }
@ -856,8 +861,7 @@
<div class="brand"> <div class="brand">
<span class="brand-mark">◢◤</span> <span class="brand-mark">◢◤</span>
<div> <div>
<div class="brand-name">OSINT<span class="accent">//</span>DASHBOARD</div> <div class="brand-name">OSINT<span class="accent">//</span></div>
<div class="brand-sub">GLOBAL SITUATIONAL AWARENESS TERMINAL</div>
</div> </div>
</div> </div>
<div class="topbar-right"> <div class="topbar-right">
@ -906,7 +910,7 @@
<button class="btn" id="map-30d-btn" onclick="mapGoDays(30)" style="display:none">-30d</button> <button class="btn" id="map-30d-btn" onclick="mapGoDays(30)" style="display:none">-30d</button>
<button class="btn" id="map-reset" onclick="mapResetView()">Reset</button> <button class="btn" id="map-reset" onclick="mapResetView()">Reset</button>
<span class="mt-label" id="chokepoint-label">Strait</span> <span class="mt-label" id="chokepoint-label">Strait</span>
<div class="chokepoint-btns" id="chokepoint-btns" role="group" aria-label="Chokepoint presets"></div> <div class="chokepoint-btns" id="chokepoint-btns" hidden role="group" aria-hidden="true"></div>
<select id="chokepoint-select" aria-label="Chokepoint preset" onchange="applyChokepoint(this.value)"> <select id="chokepoint-select" aria-label="Chokepoint preset" onchange="applyChokepoint(this.value)">
<option value="">Strait…</option> <option value="">Strait…</option>
</select> </select>
@ -915,8 +919,8 @@
<div id="map"></div> <div id="map"></div>
<!-- Layer control panel --> <!-- Layer control panel -->
<div id="layer-panel"> <div id="layer-panel" class="collapsed">
<div class="lp-head" onclick="toggleLayerPanel()" role="button" tabindex="0" aria-expanded="true" <div class="lp-head" onclick="toggleLayerPanel()" role="button" tabindex="0" aria-expanded="false"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleLayerPanel();}"> onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleLayerPanel();}">
◈ Layers <span class="lp-caret"></span> ◈ Layers <span class="lp-caret"></span>
</div> </div>
@ -928,12 +932,20 @@
</div> </div>
<div class="lp-opacity">Opacity <input type="range" id="lp-base-opacity" min="5" max="100" value="100" oninput="setBaseOpacity(this.value)"><b id="lp-base-val">100%</b></div> <div class="lp-opacity">Opacity <input type="range" id="lp-base-opacity" min="5" max="100" value="100" oninput="setBaseOpacity(this.value)"><b id="lp-base-val">100%</b></div>
</div> </div>
<div class="lp-layer">
<div class="lp-row gf-draw-row">
<button class="btn" id="gf-draw" type="button" onclick="toggleGeofenceDraw()" title="Tap vertices, then Finish">Draw geofence</button>
<button class="btn" id="gf-finish" type="button" hidden onclick="onGfClose(event)">Finish</button>
<button class="btn" id="gf-cancel" type="button" hidden onclick="cancelGeofenceDraw()">Cancel</button>
</div>
<div id="gf-list" class="gf-list"></div>
<div id="gf-inbox" class="gf-inbox" aria-label="Geofence alerts"></div>
</div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-terminator-on" onchange="toggleTerminator()"> <span class="lp-dot night"></span> Day/Night</label> <label class="lp-name" title="Sun terminator. No API."><input type="checkbox" id="lp-terminator-on" onchange="toggleTerminator()"> <span class="lp-dot night"></span> Day/Night</label>
<span class="lp-count" id="lp-terminator-count">off</span> <span class="lp-count" id="lp-terminator-count">off</span>
</div> </div>
<div class="lp-note">Sun terminator overlay. Default off. No API.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
@ -962,14 +974,14 @@
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-cams-on" checked onchange="toggleCams()"> <span class="lp-dot cams"></span> Open Cameras</label> <label class="lp-name"><input type="checkbox" id="lp-cams-on" onchange="toggleCams()"> <span class="lp-dot cams"></span> Open Cameras</label>
<span class="lp-count" id="lp-cams-count">0</span> <span class="lp-count" id="lp-cams-count">0</span>
</div> </div>
<div class="lp-opacity">Opacity <input type="range" id="lp-cams-opacity" min="5" max="100" value="100" oninput="setCamsOpacity(this.value)"><b id="lp-cams-val">100%</b></div> <div class="lp-opacity">Opacity <input type="range" id="lp-cams-opacity" min="5" max="100" value="100" oninput="setCamsOpacity(this.value)"><b id="lp-cams-val">100%</b></div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-blips-on" checked onchange="toggleBlips()"> <span class="lp-dot blips"></span> Event Blips</label> <label class="lp-name" title="Geolocated ingest events"><input type="checkbox" id="lp-blips-on" onchange="toggleBlips()"> <span class="lp-dot blips"></span> Event Blips</label>
<span class="lp-count" id="lp-blips-count">0</span> <span class="lp-count" id="lp-blips-count">0</span>
</div> </div>
<div class="lp-sub"> <div class="lp-sub">
@ -983,27 +995,24 @@
</select> </select>
</label> </label>
</div> </div>
<div class="lp-note">Geolocated events from the ingest pipeline. Color = source type.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-news-on" checked onchange="toggleNewsPins()"> <span class="lp-dot news"></span> Critical News</label> <label class="lp-name" title="LLM-estimated locations from the 15-min brief"><input type="checkbox" id="lp-news-on" onchange="toggleNewsPins()"> <span class="lp-dot news"></span> Critical News</label>
<span class="lp-count" id="lp-news-count">0</span> <span class="lp-count" id="lp-news-count">0</span>
</div> </div>
<div class="lp-note">LLM-estimated locations from the 15-min brief. Pins only for critical/high.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-radar-on" checked onchange="toggleRadar()"> <span class="lp-dot radar"></span> Radar</label> <label class="lp-name" title="IEM NEXRAD over CONUS, RainViewer elsewhere"><input type="checkbox" id="lp-radar-on" onchange="toggleRadar()"> <span class="lp-dot radar"></span> Radar</label>
<span class="lp-count" id="lp-radar-count">IEM</span> <span class="lp-count" id="lp-radar-count">IEM</span>
</div> </div>
<div class="lp-opacity">Opacity <input type="range" id="lp-radar-opacity" min="5" max="100" value="70" oninput="setRadarOpacity(this.value)"><b id="lp-radar-val">70%</b></div> <div class="lp-opacity">Opacity <input type="range" id="lp-radar-opacity" min="5" max="100" value="70" oninput="setRadarOpacity(this.value)"><b id="lp-radar-val">70%</b></div>
<div class="lp-note">IEM NEXRAD over CONUS, RainViewer elsewhere. Tiles load in the browser.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-alerts-on" checked onchange="toggleWxAlerts()"> <span class="lp-dot weather"></span> NWS / SBW Alerts</label> <label class="lp-name"><input type="checkbox" id="lp-alerts-on" onchange="toggleWxAlerts()"> <span class="lp-dot weather"></span> NWS / SBW Alerts</label>
<span class="lp-count" id="lp-alerts-count">0</span> <span class="lp-count" id="lp-alerts-count">0</span>
</div> </div>
</div> </div>
@ -1015,15 +1024,14 @@
</div> </div>
<div class="lp-layer" id="sentinel-1-sar"> <div class="lp-layer" id="sentinel-1-sar">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-sentinel-on" onchange="toggleSentinel1()"> <span class="lp-dot sar"></span> Sentinel-1 SAR (Cloud-Penetrating)</label> <label class="lp-name" title="C-band SAR, last 7 days, viewport"><input type="checkbox" id="lp-sentinel-on" onchange="toggleSentinel1()"> <span class="lp-dot sar"></span> Sentinel-1 SAR (Cloud-Penetrating)</label>
<span class="lp-count" id="lp-sentinel-count">7d</span> <span class="lp-count" id="lp-sentinel-count">7d</span>
</div> </div>
<div class="lp-opacity">Opacity <input type="range" id="lp-sentinel-opacity" min="5" max="100" value="80" oninput="setSentinelOpacity(this.value)"><b id="lp-sentinel-val">80%</b></div> <div class="lp-opacity">Opacity <input type="range" id="lp-sentinel-opacity" min="5" max="100" value="80" oninput="setSentinelOpacity(this.value)"><b id="lp-sentinel-val">80%</b></div>
<div class="lp-note">Cloud-penetrating C-band. Last 7 days, viewport bbox. Tiles from same-origin TiTiler — not fetched on load.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-perim-on" checked onchange="togglePerimeters()"> <span class="lp-dot wfigs"></span> WFIGS Perimeters</label> <label class="lp-name"><input type="checkbox" id="lp-perim-on" onchange="togglePerimeters()"> <span class="lp-dot wfigs"></span> WFIGS Perimeters</label>
<span class="lp-count" id="lp-perim-count">0</span> <span class="lp-count" id="lp-perim-count">0</span>
</div> </div>
</div> </div>
@ -1035,51 +1043,37 @@
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-ac-on" checked onchange="toggleAircraft()"> <span class="lp-dot flights"></span> Aircraft</label> <label class="lp-name" title="MIL / firefighting / emergency squawk styling"><input type="checkbox" id="lp-ac-on" onchange="toggleAircraft()"> <span class="lp-dot flights"></span> Aircraft</label>
<span class="lp-count" id="lp-ac-count">0</span> <span class="lp-count" id="lp-ac-count">0</span>
</div> </div>
<div class="lp-row" id="lp-ac-mil-row" hidden> <div class="lp-row" id="lp-ac-mil-row" hidden>
<label class="lp-name"><input type="checkbox" id="lp-ac-mil-on" onchange="toggleAircraftMil()"> <span class="lp-dot mil"></span> MIL</label> <label class="lp-name"><input type="checkbox" id="lp-ac-mil-on" onchange="toggleAircraftMil()"> <span class="lp-dot mil"></span> MIL</label>
<span class="lp-count" id="lp-ac-mil-count"></span> <span class="lp-count" id="lp-ac-mil-count"></span>
</div> </div>
<div class="lp-note">Magenta = military · orange = firefighting · red = emergency (7700/7600/7500) · else altitude. Click a plane for type / squawk / operator.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-trains-on" checked onchange="toggleTrains()"> <span class="lp-dot trains"></span> Trains</label> <label class="lp-name" title="Amtraker · ODC-By"><input type="checkbox" id="lp-trains-on" onchange="toggleTrains()"> <span class="lp-dot trains"></span> Trains</label>
<span class="lp-count" id="lp-trains-count">0</span> <span class="lp-count" id="lp-trains-count">0</span>
</div> </div>
<div class="lp-note">Amtraker · ODC-By</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-vessels-on" onchange="toggleVessels()"> <span class="lp-dot vessels"></span> Vessels · AIS</label> <label class="lp-name" title="AISStream US live · VesselAPI Hormuz"><input type="checkbox" id="lp-vessels-on" onchange="toggleVessels()"> <span class="lp-dot vessels"></span> Vessels · AIS</label>
<span class="lp-count" id="lp-vessels-count">0</span> <span class="lp-count" id="lp-vessels-count">0</span>
</div> </div>
<div class="lp-note">Two feeds: AISStream (US live) and VesselAPI (Hormuz, 5×/day). Paste both keys on the Keys page. Magenta = military · gold = law/gov · teal = underway. Click for type / dest / IMO.</div>
</div>
<div class="lp-layer">
<div class="lp-row gf-draw-row">
<button class="btn" id="gf-draw" type="button" onclick="toggleGeofenceDraw()">Draw geofence</button>
<button class="btn" id="gf-finish" type="button" hidden onclick="onGfClose(event)">Finish</button>
<button class="btn" id="gf-cancel" type="button" hidden onclick="cancelGeofenceDraw()">Cancel</button>
</div>
<div id="gf-list" class="gf-list"></div>
<div id="gf-inbox" class="gf-inbox" aria-label="Geofence alerts"></div>
<div class="lp-note">Tap vertices, then Finish (or double-click). Inbox keeps hits after the toast fades.</div>
</div> </div>
<div class="lp-layer"> <div class="lp-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-storms-on" checked onchange="toggleStorms()"> <span class="lp-dot storms"></span> NHC Storms</label> <label class="lp-name"><input type="checkbox" id="lp-storms-on" onchange="toggleStorms()"> <span class="lp-dot storms"></span> NHC Storms</label>
<span class="lp-count" id="lp-storms-count">0</span> <span class="lp-count" id="lp-storms-count">0</span>
</div> </div>
</div> </div>
<div class="lp-layer" id="conflicts-layer"> <div class="lp-layer" id="conflicts-layer">
<div class="lp-row"> <div class="lp-row">
<label class="lp-name"><input type="checkbox" id="lp-conflicts-on" onchange="toggleConflicts()"> <span class="lp-dot conflicts"></span> Conflicts</label> <label class="lp-name" title="Curated theatres · news counts"><input type="checkbox" id="lp-conflicts-on" onchange="toggleConflicts()"> <span class="lp-dot conflicts"></span> Conflicts</label>
<span class="lp-count" id="lp-conflicts-count">0</span> <span class="lp-count" id="lp-conflicts-count">0</span>
</div> </div>
<div class="lp-note">Curated theatres · color by severity · news counts, not LiveUAMap.</div>
</div> </div>
<div class="lp-legend"> <div class="lp-legend">
@ -1121,7 +1115,7 @@
<dt><kbd>R</kbd></dt><dd>Reset map view</dd> <dt><kbd>R</kbd></dt><dd>Reset map view</dd>
<dt><kbd>Esc</kbd></dt><dd>Close panels</dd> <dt><kbd>Esc</kbd></dt><dd>Close panels</dd>
</dl> </dl>
<p class="lp-note" style="margin-top:0.7rem">Camera / HLS thumbs at zoom 12+. F and S are left unbound.</p> <p class="cheat-note" style="margin-top:0.7rem">Camera / HLS thumbs at zoom 12+. F and S are left unbound.</p>
</div> </div>
<div id="dvr-bar" class="live"> <div id="dvr-bar" class="live">
<button class="btn" id="dvr-live" type="button" onclick="dvrGoLive()">LIVE</button> <button class="btn" id="dvr-live" type="button" onclick="dvrGoLive()">LIVE</button>
@ -1355,8 +1349,8 @@
<div class="set-row"> <div class="set-row">
<label>Layers on by default</label> <label>Layers on by default</label>
<div style="display:flex;gap:1.2rem;font-size:0.82rem"> <div style="display:flex;gap:1.2rem;font-size:0.82rem">
<label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="set-cams-on" checked style="accent-color:var(--cyan)"> Cameras</label> <label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="set-cams-on" style="accent-color:var(--cyan)"> Cameras</label>
<label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="set-blips-on" checked style="accent-color:var(--cyan)"> Blips</label> <label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="set-blips-on" style="accent-color:var(--cyan)"> Blips</label>
<label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="set-fires-on" checked style="accent-color:var(--cyan)"> Fires</label> <label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="set-fires-on" checked style="accent-color:var(--cyan)"> Fires</label>
</div> </div>
</div> </div>
@ -1387,8 +1381,8 @@
</main> </main>
<!-- ═════════════ BOTTOM DOCK: TICKERS ═════════════ --> <!-- ═════════════ BOTTOM DOCK: TICKERS ═════════════ -->
<footer class="dock chrome"> <footer class="dock chrome news-only">
<div class="ticker market" aria-label="Market data ticker"> <div class="ticker market" hidden aria-hidden="true" aria-label="Market data ticker">
<div class="tick-label"><span class="st"></span> MKT</div> <div class="tick-label"><span class="st"></span> MKT</div>
<div class="tick-wrap"> <div class="tick-wrap">
<div class="tick-track" id="mkt-track"></div> <div class="tick-track" id="mkt-track"></div>
@ -1483,10 +1477,20 @@ const STATS_COUNT_IDS = {
alerts: 'lp-alerts-count', alerts: 'lp-alerts-count',
}; };
const COUNT_HUD = {
'lp-fires-count': 'hud-fires-n',
'lp-cams-count': 'hud-cams-n',
'lp-blips-count': 'hud-blips-n',
'lp-news-count': 'hud-news-n',
};
function setLayerCount(id, value) { function setLayerCount(id, value) {
if (statsRailActive && Object.values(STATS_COUNT_IDS).indexOf(id) !== -1) return;
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) el.textContent = value; if (el) el.textContent = value;
const hid = COUNT_HUD[id];
if (hid) {
const h = document.getElementById(hid);
if (h) h.textContent = value;
}
} }
async function pollLayerStats() { async function pollLayerStats() {
@ -1498,11 +1502,19 @@ async function pollLayerStats() {
const s = await r.json(); const s = await r.json();
if (!s || typeof s !== 'object') return; if (!s || typeof s !== 'object') return;
statsRailActive = true; statsRailActive = true;
const skipIfOn = {
fires: firesOn,
cameras: camsOn,
aircraft: acOn,
vessels: vesselsOn,
trains: trainsOn,
alerts: wxAlertsOn,
};
Object.keys(STATS_COUNT_IDS).forEach((k) => { Object.keys(STATS_COUNT_IDS).forEach((k) => {
if (skipIfOn[k]) return;
const n = s[k]; const n = s[k];
if (typeof n !== 'number' || !isFinite(n)) return; if (typeof n !== 'number' || !isFinite(n)) return;
const el = document.getElementById(STATS_COUNT_IDS[k]); setLayerCount(STATS_COUNT_IDS[k], n.toLocaleString());
if (el) el.textContent = n.toLocaleString();
}); });
} catch (e) { } catch (e) {
// Never block the map on stats failure — keep last viewport lengths. // Never block the map on stats failure — keep last viewport lengths.
@ -1758,6 +1770,11 @@ function initMarketTicker() {
buildMarketTrack(true); buildMarketTrack(true);
setMarketTag('standby'); setMarketTag('standby');
// GET /api/market is not wired — do not 404-poll every 15s. // GET /api/market is not wired — do not 404-poll every 15s.
const el = document.querySelector('.ticker.market');
if (el) { el.hidden = true; el.setAttribute('aria-hidden', 'true'); }
const dock = document.querySelector('.dock');
if (dock) dock.classList.add('news-only');
document.documentElement.style.setProperty('--dock-h', '32px');
} }
/* ═══════════════ NEWS FEED + TICKER ═══════════════ */ /* ═══════════════ NEWS FEED + TICKER ═══════════════ */
@ -2135,37 +2152,38 @@ function resetMapSettings() {
try { localStorage.removeItem('osint.mapDefaults'); } catch (e) {} try { localStorage.removeItem('osint.mapDefaults'); } catch (e) {}
document.getElementById('set-fires-since').value = '24h'; document.getElementById('set-fires-since').value = '24h';
document.getElementById('set-blips-since').value = '24h'; document.getElementById('set-blips-since').value = '24h';
document.getElementById('set-cams-on').checked = true; document.getElementById('set-cams-on').checked = false;
document.getElementById('set-blips-on').checked = true; document.getElementById('set-blips-on').checked = false;
document.getElementById('set-fires-on').checked = true; document.getElementById('set-fires-on').checked = true;
applyMapSettings({firesSince:'24h', blipsSince:'24h', camsOn:true, blipsOn:true, firesOn:true}, true); applyMapSettings({firesSince:'24h', blipsSince:'24h', camsOn:false, blipsOn:false, firesOn:true}, true);
document.getElementById('map-saved').textContent = 'Defaults restored.'; document.getElementById('map-saved').textContent = 'Defaults restored.';
} }
function applyMapSettings(prefs, live) { function applyMapSettings(prefs, live) {
prefs = prefs || {};
const fs = document.getElementById('lp-fires-since'); const fs = document.getElementById('lp-fires-since');
const bs = document.getElementById('lp-blips-since'); const bs = document.getElementById('lp-blips-since');
if (fs) fs.value = prefs.firesSince || '24h'; if (fs && prefs.firesSince) fs.value = prefs.firesSince;
if (bs) bs.value = prefs.blipsSince || '24h'; if (bs && prefs.blipsSince) bs.value = prefs.blipsSince;
const cf = document.getElementById('lp-cams-on'); const cf = document.getElementById('lp-cams-on');
const bf = document.getElementById('lp-blips-on'); const bf = document.getElementById('lp-blips-on');
const ff = document.getElementById('lp-fires-on'); const ff = document.getElementById('lp-fires-on');
if (cf) cf.checked = prefs.camsOn !== false; if (cf && Object.prototype.hasOwnProperty.call(prefs, 'camsOn')) cf.checked = !!prefs.camsOn;
if (bf) bf.checked = prefs.blipsOn !== false; if (bf && Object.prototype.hasOwnProperty.call(prefs, 'blipsOn')) bf.checked = !!prefs.blipsOn;
if (ff) ff.checked = prefs.firesOn !== false; if (ff && Object.prototype.hasOwnProperty.call(prefs, 'firesOn')) ff.checked = !!prefs.firesOn;
if (!live) return; if (!live) return;
if (map) { if (map) {
if (prefs.camsOn === false && camsGroup) { map.removeLayer(camsGroup); camsGroup = null; } if (prefs.camsOn === false && camsGroup) { map.removeLayer(camsGroup); camsGroup = null; }
if (prefs.blipsOn === false && blipsGroup) { map.removeLayer(blipsGroup); blipsGroup = null; } if (prefs.blipsOn === false && blipsGroup) { map.removeLayer(blipsGroup); blipsGroup = null; }
if (prefs.firesOn === false && firesHeat) { map.removeLayer(firesHeat); firesHeat = null; } if (prefs.firesOn === false && firesHeat) { map.removeLayer(firesHeat); firesHeat = null; }
if (prefs.firesOn !== false && firesOn) loadFires(); if (prefs.firesOn && firesOn) loadFires();
if (prefs.camsOn !== false && camsOn) loadCams(); if (prefs.camsOn && camsOn) loadCams();
if (prefs.blipsOn !== false && blipsOn) loadBlips(); if (prefs.blipsOn && blipsOn) loadBlips();
} }
firesSince = prefs.firesSince || firesSince; if (prefs.firesSince) firesSince = prefs.firesSince;
blipsSince = prefs.blipsSince || blipsSince; if (prefs.blipsSince) blipsSince = prefs.blipsSince;
camsOn = prefs.camsOn !== false; if (Object.prototype.hasOwnProperty.call(prefs, 'camsOn')) camsOn = !!prefs.camsOn;
blipsOn = prefs.blipsOn !== false; if (Object.prototype.hasOwnProperty.call(prefs, 'blipsOn')) blipsOn = !!prefs.blipsOn;
firesOn = prefs.firesOn !== false; if (Object.prototype.hasOwnProperty.call(prefs, 'firesOn')) firesOn = !!prefs.firesOn;
syncHud(); syncHud();
} }
function initSettings() { function initSettings() {
@ -2246,17 +2264,17 @@ let mapDomain = null;
let mapLatest = null; let mapLatest = null;
let mapAttributionAdded = null; let mapAttributionAdded = null;
let extraAttribs = new Set(); let extraAttribs = new Set();
let radarLayer = null, radarOn = true, radarOpacity = 0.7, radarMeta = null, radarTimer = null; let radarLayer = null, radarOn = false, radarOpacity = 0.7, radarMeta = null, radarTimer = null;
let thermalLayer = null, thermalOn = false; let thermalLayer = null, thermalOn = false;
let sentinelLayer = null, sentinelOn = false, sentinelOpacity = 0.8; let sentinelLayer = null, sentinelOn = false, sentinelOpacity = 0.8;
let sentinelItemId = null, sentinelBounds = null; let sentinelItemId = null, sentinelBounds = null;
let wxAlertsGroup = null, wxAlertsOn = true; let wxAlertsGroup = null, wxAlertsOn = false;
let perimGroup = null, perimOn = true; let perimGroup = null, perimOn = false;
let incidentsGroup = null, incidentsOn = false; let incidentsGroup = null, incidentsOn = false;
let acGroup = null, acOn = true, acMilOn = false, acMilSupported = false; let acGroup = null, acOn = false, acMilOn = false, acMilSupported = false;
let trainsGroup = null, trainsOn = true; let trainsGroup = null, trainsOn = false;
let vesselsGroup = null, vesselsOn = false; let vesselsGroup = null, vesselsOn = false;
let stormsGroup = null, stormsOn = true; let stormsGroup = null, stormsOn = false;
let conflictsGroup = null, conflictsOn = false, conflictsCache = null; let conflictsGroup = null, conflictsOn = false, conflictsCache = null;
let overlayReq = {ac:0, trains:0, vessels:0, alerts:0, perim:0, incidents:0, storms:0, sar:0, conflicts:0}; let overlayReq = {ac:0, trains:0, vessels:0, alerts:0, perim:0, incidents:0, storms:0, sar:0, conflicts:0};
let moveDebounce = null; let moveDebounce = null;
@ -2416,7 +2434,9 @@ async function initMap() {
sel.innerHTML = mapLayers.map(l => sel.innerHTML = mapLayers.map(l =>
`<option value="${l.id}">${l.title}</option>`).join(''); `<option value="${l.id}">${l.title}</option>`).join('');
const preferred = const preferred =
mapLayers.find(l => l.id === 'BlueMarble_ShadedRelief_Bathymetry') mapLayers.find(l => l.id === 'VIIRS_SNPP_CorrectedReflectance_TrueColor')
|| mapLayers.find(l => l.id === 'MODIS_Terra_CorrectedReflectance_TrueColor')
|| mapLayers.find(l => l.id === 'BlueMarble_ShadedRelief_Bathymetry')
|| mapLayers.find(l => !l.has_time) || mapLayers.find(l => !l.has_time)
|| mapLayers[0]; || mapLayers[0];
if (preferred) sel.value = preferred.id; if (preferred) sel.value = preferred.id;
@ -2529,7 +2549,7 @@ async function initMap() {
}); });
// Static Blue Marble has no time-domain fetch — don't block overlays on GIBS. // Static Blue Marble has no time-domain fetch — don't block overlays on GIBS.
mapLayerChanged(); mapLayerChanged();
// Overlays default ON (checkboxes in the layer panel + saved settings) // Overlays follow layer-panel checkboxes (basemap + FIRMS default).
applyMapSettings(readMapPrefs(), false); applyMapSettings(readMapPrefs(), false);
firesOn = document.getElementById('lp-fires-on').checked; firesOn = document.getElementById('lp-fires-on').checked;
camsOn = document.getElementById('lp-cams-on').checked; camsOn = document.getElementById('lp-cams-on').checked;
@ -2571,16 +2591,14 @@ function readMapPrefs() {
} catch (e) { return {}; } } catch (e) { return {}; }
} }
function syncHud() { function syncHud() {
const set = (id, n, on) => { const set = (id, on) => {
const chip = document.getElementById('hud-' + id); const chip = document.getElementById('hud-' + id);
const num = document.getElementById('hud-' + id + '-n');
if (num) num.textContent = n == null ? '—' : Number(n).toLocaleString();
if (chip) chip.classList.toggle('off', on === false); if (chip) chip.classList.toggle('off', on === false);
}; };
set('fires', firesOn ? (hudFiresCount) : '—', firesOn); set('fires', firesOn);
set('cams', camsOn ? (hudCamsCount) : '—', camsOn); set('cams', camsOn);
set('blips', blipsOn ? (hudBlipsCount) : '—', blipsOn); set('blips', blipsOn);
set('news', newsOn ? (hudNewsCount) : '—', newsOn); set('news', newsOn);
} }
let hudFiresCount = null, hudCamsCount = null, hudBlipsCount = null, hudNewsCount = null; let hudFiresCount = null, hudCamsCount = null, hudBlipsCount = null, hudNewsCount = null;
@ -3139,7 +3157,7 @@ async function loadBlips() {
`</div>`); `</div>`);
})); }));
blipsGroup.addTo(map); blipsGroup.addTo(map);
document.getElementById('lp-blips-count').textContent = evs.length.toLocaleString(); setLayerCount('lp-blips-count', evs.length.toLocaleString());
hudBlipsCount = evs.length; hudBlipsCount = evs.length;
syncHud(); syncHud();
if (document.getElementById('map-hint').textContent.includes('fire hotspots')) { if (document.getElementById('map-hint').textContent.includes('fire hotspots')) {
@ -3165,8 +3183,7 @@ async function toggleNewsPins() {
newsReq++; newsReq++;
if (newsGroup && map) { map.removeLayer(newsGroup); newsGroup = null; } if (newsGroup && map) { map.removeLayer(newsGroup); newsGroup = null; }
hudNewsCount = null; hudNewsCount = null;
const countEl = document.getElementById('lp-news-count'); setLayerCount('lp-news-count', '0');
if (countEl) countEl.textContent = '0';
syncHud(); syncHud();
} }
async function loadNewsPins() { async function loadNewsPins() {
@ -3179,15 +3196,13 @@ async function loadNewsPins() {
if (req !== newsReq) return; // superseded by a newer pan/zoom if (req !== newsReq) return; // superseded by a newer pan/zoom
const items = Array.isArray(data) ? data : []; const items = Array.isArray(data) ? data : [];
if (newsGroup) map.removeLayer(newsGroup); if (newsGroup) map.removeLayer(newsGroup);
const col = '#ff2e97'; const col = '#35e0ff';
const pins = items.filter(it => Number.isFinite(Number(it.lat)) && Number.isFinite(Number(it.lon))); const pins = items.filter(it => Number.isFinite(Number(it.lat)) && Number.isFinite(Number(it.lon)));
newsGroup = L.layerGroup(pins.map(it => { newsGroup = L.layerGroup(pins.map(it => {
const icon = L.divIcon({ return L.circleMarker([it.lat, it.lon], {
className: '', radius: 6, weight: 1, color: col, fillColor: col, fillOpacity: 0.7,
html: `<span style="display:inline-block;width:10px;height:10px;background:${col};box-shadow:0 0 8px ${col},0 0 2px #0f172a;border:1px solid #030609;transform:rotate(45deg);"></span>`, renderer: pointCanvas(),
iconSize: [14, 14], iconAnchor: [7, 7], })
});
return L.marker([it.lat, it.lon], { icon })
.bindPopup(`<div class="blip-pop">` + .bindPopup(`<div class="blip-pop">` +
`<div class="blip-src">◈ ${esc(it.importance || '')}${it.category ? ' · ' + esc(it.category) : ''}</div>` + `<div class="blip-src">◈ ${esc(it.importance || '')}${it.category ? ' · ' + esc(it.category) : ''}</div>` +
`<b>${esc(it.headline || '')}</b>` + `<b>${esc(it.headline || '')}</b>` +
@ -3196,7 +3211,7 @@ async function loadNewsPins() {
`</div>`); `</div>`);
})); }));
newsGroup.addTo(map); newsGroup.addTo(map);
document.getElementById('lp-news-count').textContent = pins.length.toLocaleString(); setLayerCount('lp-news-count', pins.length.toLocaleString());
hudNewsCount = pins.length; hudNewsCount = pins.length;
syncHud(); syncHud();
} catch(e) { } catch(e) {

113
tests/test_hud_chrome.py Normal file
View file

@ -0,0 +1,113 @@
"""Quiet HUD chrome: VIIRS default, collapsed rail, no Orbitron/MKT dashes."""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
HTML = (ROOT / "app/static/index.html").read_text()
def _attr(html: str, elem_id: str) -> str:
chunk = html.split(f'id="{elem_id}"', 1)[1].split(">", 1)[0]
return chunk
def test_initmap_prefers_viirs_true_color():
init = HTML.split("async function initMap", 1)[1].split("function readMapPrefs", 1)[0]
assert "VIIRS_SNPP_CorrectedReflectance_TrueColor" in init
assert init.index("VIIRS_SNPP_CorrectedReflectance_TrueColor") < init.index(
"MODIS_Terra_CorrectedReflectance_TrueColor"
)
assert init.index("MODIS_Terra_CorrectedReflectance_TrueColor") < init.index(
"BlueMarble_ShadedRelief_Bathymetry"
)
def test_orbitron_gone():
assert "Orbitron" not in HTML
assert "IBM Plex Sans" in HTML
assert "IBM Plex Mono" in HTML
def test_lp_note_stripped_from_layer_list():
assert 'class="lp-note"' not in HTML
body = HTML.split('class="lp-body"', 1)[1].split("lp-legend", 1)[0]
assert "lp-note" not in body
def test_default_overlays_basemap_and_firms_only():
fires = _attr(HTML, "lp-fires-on")
assert "checked" in fires
for eid in (
"lp-cams-on",
"lp-blips-on",
"lp-news-on",
"lp-radar-on",
"lp-alerts-on",
"lp-perim-on",
"lp-ac-on",
"lp-trains-on",
"lp-storms-on",
):
assert "checked" not in _attr(HTML, eid), eid
def test_geofence_markup_before_cameras():
assert 'id="gf-draw"' in HTML
assert HTML.index('id="gf-draw"') < HTML.index('id="lp-cams-on"')
assert HTML.index('id="lp-base-on"') < HTML.index('id="gf-draw"')
def test_parent_geofence_hud_survives():
assert "watch_geofences" in HTML
assert "function deleteGeofence" in HTML
assert 'id="gf-finish"' in HTML
assert 'id="gf-cancel"' in HTML
assert 'id="gf-inbox"' in HTML
def test_layer_rail_collapsed_on_load():
head = HTML.split('class="lp-head"', 1)[1].split("</div>", 1)[0]
assert 'aria-expanded="false"' in head
assert 'id="layer-panel" class="collapsed"' in HTML
def test_market_ticker_hidden_no_poll():
mkt = HTML.split('class="ticker market"', 1)[1].split(">", 1)[0]
assert "hidden" in mkt
assert "setInterval(probeMarket" not in HTML
assert "setInterval(loadMarket" not in HTML
init = HTML.split("function initMarketTicker", 1)[1].split("function ", 1)[0]
assert "/api/market" in init or "404-poll" in init
assert "setInterval" not in init
def test_news_ticker_fills_news_only_dock():
css = HTML.split("</style>", 1)[0]
compact = css.replace(" ", "").replace("\n", "")
assert ".dock.news-only{height:32px;}" in compact
assert ".dock.news-only.ticker{height:100%;}" in compact
assert ".ticker{display:flex;align-items:stretch;height:50%;" in compact
def test_news_pins_are_circle_markers():
js = HTML.split("async function loadNewsPins", 1)[1].split("function refreshLiveOverlays", 1)[0]
assert "L.circleMarker" in js
assert "fillOpacity: 0.7" in js or "fillOpacity:0.7" in js
assert "rotate(45deg)" not in js
assert "L.divIcon" not in js
def test_chokepoint_buttons_not_in_toolbar_flow():
assert 'id="chokepoint-select"' in HTML
css = HTML.split("</style>", 1)[0]
assert ".chokepoint-btns { display: none; }" in css or ".chokepoint-btns{display:none" in css.replace(
" ", ""
)
def test_brand_is_osint_slash():
assert "GLOBAL SITUATIONAL AWARENESS TERMINAL" not in HTML
assert "OSINT" in HTML
assert 'class="accent">//</span>' in HTML