edgarodriguez commited on
Commit
ac14fe0
·
verified ·
1 Parent(s): 85c80a5

Upload 2 files

Browse files
Files changed (2) hide show
  1. depth_interactive_map_alpha.R +392 -71
  2. styles.css +134 -16
depth_interactive_map_alpha.R CHANGED
@@ -321,17 +321,72 @@ WEB_SQL_TEMPLATES <- list(
321
  "SELECT wkt FROM state_polygons WHERE CVE_ENT = '{cve_ent}' LIMIT 1",
322
 
323
  temporal_mig_all =
324
- "SELECT {yr_expr} AS yr, COUNT(*) AS n FROM migrants
 
 
 
325
  WHERE incident_date_ts IS NOT NULL
326
  GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
327
 
328
  temporal_mig_sel =
329
- "SELECT {yr_expr} AS yr, COUNT(*) AS n FROM migrants
 
 
 
330
  WHERE incident_date_ts IS NOT NULL
331
  AND lon IS NOT NULL AND lat IS NOT NULL
332
  AND ST_Within(ST_Point(lon, lat), ST_GeomFromText('{poly_wkt}'))
333
  GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  temporal_yr_all =
336
  "SELECT {yr_expr} AS yr, COUNT(*) AS n FROM {tbl}
337
  WHERE {yr_col} IS NOT NULL
@@ -548,7 +603,7 @@ legend_bounds <- function(pal_name, unit = "") {
548
  #######################################################################################
549
 
550
  .NOMINATIM_CACHE <- new.env(parent = emptyenv())
551
- .NOMINATIM_UA <- "DEPTH-RiskMap/1.0 (Rights Lab, University of Nottingham)"
552
 
553
  nominatim_search <- function(q, limit = 6, country = "mx") {
554
  q <- trimws(q %||% "")
@@ -695,7 +750,7 @@ WEB_SPEC <- list(
695
  legend = list(type = "ramp", pal = "desap", unit = " /100k")),
696
  list(kind = "layer", value = "fosas-choro",
697
  label = "Clandestine burial pits by municipality",
698
- tooltip = "Number of clandestine burial pits (fosas) and bodies found per municipality. Source: RNPED.",
699
  legend = list(type = "ramp", pal = "fosas", unit = " pits")),
700
  list(kind = "radios", group = "fosas"),
701
  list(kind = "layer", value = "graves-pts",
@@ -775,8 +830,14 @@ WEB_SPEC <- list(
775
  )),
776
  list(title = "Enforced Disappearances (RNPDNO)", kind = "stat",
777
  stats = list(list(id = "n_desap", label = "Registered disappearances", color = COL_RISK))),
778
- list(title = "Burial Data", kind = "stat",
 
779
  stats = list(list(id = "n_graves", label = "Clandestine burial sites", color = COL_RED))),
 
 
 
 
 
780
  list(title = "Organised Crime Violence (OCVED 2.0)", kind = "stat", chart = "temporal_ocved",
781
  stats = list(list(id = "ocved_n", label = "Recorded violence events", color = COL_AMBER))),
782
  list(title = "Armed Conflict Events (UCDP-GED)", kind = "stat", chart = "temporal_ged",
@@ -784,12 +845,15 @@ WEB_SPEC <- list(
784
  list(title = "Criminal Incidence by Municipality", kind = "munlist")
785
  )),
786
  list(id = "exposure", label = "Exposure", open = FALSE, cards = list(
787
- list(title = "Irregular Migration Events (PAM)", kind = "stat",
788
- stats = list(list(id = "n_pam", label = "Recorded irregular migration events", color = COL_COUNT)))
 
 
789
  )),
790
  list(id = "vulnerability", label = "Vulnerability", open = FALSE, cards = list(
791
- list(title = "Migrant Support Facilities", kind = "stat",
792
- stats = list(list(id = "n_facilities", label = "Active support facilities", color = COL_GREEN)))
 
793
  ))
794
  )
795
  )
@@ -839,12 +903,15 @@ LUCIDE_JS <- tags$head(
839
  Shiny.setInputValue('grid_nivel', '6', { priority: 'event' });
840
  }, 300);
841
 
842
- // Relocate native mapgl draw buttons (polygon/rectangle/radius) into the
843
- // top nav-bar so they sit alongside the custom selection tools.
844
  var navTries = 0;
845
  var navTimer = setInterval(function() {
846
  navTries++;
847
- if (relocateDrawButtonsToNav() || navTries >= 40) clearInterval(navTimer);
 
 
 
848
  }, 100);
849
  });
850
 
@@ -1037,7 +1104,7 @@ LUCIDE_JS <- tags$head(
1037
  }
1038
  }
1039
 
1040
- // -- Side panel toggle (sigma / sparkles tab rail) -------------------------
1041
  function togglePanel(name) {
1042
  var panel = document.getElementById('panel-' + name);
1043
  var tab = document.getElementById('tab-' + name);
@@ -1047,12 +1114,13 @@ LUCIDE_JS <- tags$head(
1047
  panel.classList.toggle('is-collapsed', !willOpen);
1048
  tab.classList.toggle('is-active', willOpen);
1049
  tab.setAttribute('aria-selected', String(willOpen));
1050
- var tmri = document.getElementById('panel-tmri');
1051
- var assist = document.getElementById('panel-assist');
1052
- var anyOpen = (tmri && !tmri.classList.contains('is-collapsed')) ||
1053
- (assist && !assist.classList.contains('is-collapsed'));
 
1054
  if (stack) stack.classList.toggle('is-empty', !anyOpen);
1055
- // Re-render Lucide icons in case the assistance panel mounted icons just now
1056
  if (willOpen && window.lucide) {
1057
  setTimeout(function() { lucide.createIcons(); }, 30);
1058
  }
@@ -1079,41 +1147,49 @@ LUCIDE_JS <- tags$head(
1079
  } catch(e) {}
1080
  }
1081
 
1082
- // -- Native draw-button relocation -----------------------------------------
1083
- // mapgl mounts its draw control top-right inside the map. We move the
1084
- // polygon/rectangle/radius buttons into #nav-tools-select so all drawing
1085
- // and selection controls sit together in the nav-bar.
1086
- function relocateDrawButtonsToNav() {
1087
- var mapEl = document.querySelector('.maplibregl-map');
1088
- var nav = document.getElementById('nav-tools-select');
1089
- if (!mapEl || !nav) return false;
1090
- var nativeBtns = mapEl.querySelectorAll(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1091
  '.mapbox-gl-draw_polygon, .mapbox-gl-draw_rectangle, .mapbox-gl-draw_radius, .mapbox-gl-draw_line_string'
1092
  );
1093
- if (!nativeBtns.length) return false;
1094
- var firstCustom = document.getElementById('btn_mode_click');
1095
- nativeBtns.forEach(function(btn) {
1096
- if (btn.dataset.navMoved === '1') return;
1097
- btn.dataset.navMoved = '1';
1098
- btn.classList.add('nav-btn');
1099
- if (!btn.getAttribute('aria-label')) {
1100
- btn.setAttribute('aria-label', btn.getAttribute('title') || 'Draw');
1101
- }
1102
- if (firstCustom && firstCustom.parentNode === nav) {
1103
- nav.insertBefore(btn, firstCustom);
1104
- } else {
1105
- nav.appendChild(btn);
1106
- }
1107
  });
1108
- // Hide now-empty mapgl ctrl-group containers on the map
1109
- mapEl.querySelectorAll('.maplibregl-ctrl-top-right .maplibregl-ctrl-group')
1110
- .forEach(function(g) {
1111
- var hasVisible = false;
1112
- g.querySelectorAll('button').forEach(function(b) {
1113
- if (b.offsetParent !== null && !b.dataset.navMoved) hasVisible = true;
1114
- });
1115
- if (!hasVisible) g.style.display = 'none';
1116
- });
1117
  return true;
1118
  }
1119
 
@@ -1501,18 +1577,20 @@ dp_stat <- function(val_id, label, color = "#1e293b") {
1501
  )
1502
  }
1503
 
1504
- ind_card <- function(title, body, chart_id = NULL, note = NULL, soon = FALSE) {
1505
- div(class = "ind-card",
 
 
1506
  div(class = "ind-card-hdr",
1507
  span(class = "ind-card-title", title),
1508
  if (soon) tags$span(class = "acc-coming-tag", "Soon")
1509
  ),
1510
- div(class = "ind-card-body", body),
1511
- if (!is.null(note))
1512
- div(class = "ind-card-note", note),
1513
  if (!is.null(chart_id))
1514
  div(class = "ind-card-chart",
1515
- plotOutput(chart_id, width = "100%", height = "55px"))
 
 
1516
  )
1517
  }
1518
 
@@ -1592,9 +1670,17 @@ render_panel_card <- function(card) {
1592
  uiOutput("mun_list")),
1593
  stat = tagList(
1594
  lapply(card$stats, function(s)
1595
- dp_stat(s$id, s$label, s$color %||% "#1e293b")))
 
1596
  )
1597
- ind_card(card$title, body, chart_id = card$chart)
 
 
 
 
 
 
 
1598
  }
1599
 
1600
  render_panel_groups <- function() {
@@ -1656,9 +1742,37 @@ ui <- page_fillable(
1656
  div(class = "nav-divider"),
1657
 
1658
  # Selection / drawing tool group.
1659
- # Native mapgl draw buttons (rectangle/radius/polygon) are relocated into
1660
- # this container by initRelocateDrawButtons() after the map mounts.
1661
  div(class = "nav-group", id = "nav-tools-select",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1662
  tags$button(id = "btn_mode_click", class = "nav-btn",
1663
  type = "button",
1664
  `aria-label` = "Click a municipality to analyse it",
@@ -1866,6 +1980,71 @@ ui <- page_fillable(
1866
  )
1867
  ),
1868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1869
  # ---- Assistance panel (collapsed by default) -----------------------
1870
  tags$section(class = "side-panel side-panel--assist is-collapsed",
1871
  id = "panel-assist",
@@ -1923,7 +2102,18 @@ ui <- page_fillable(
1923
  `aria-label` = "Toggle Transit Migration Risk Index",
1924
  onclick = "togglePanel('tmri')",
1925
  HTML('<i data-lucide="sigma" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
1926
- tags$span(class = "side-rail-tab-label", "Index")),
 
 
 
 
 
 
 
 
 
 
 
1927
  tags$button(class = "side-rail-tab",
1928
  id = "tab-assist",
1929
  type = "button",
@@ -1983,6 +2173,7 @@ server <- function(input, output, session) {
1983
  n_events = "---", n_mun = "---",
1984
  n_desap = "---", n_pam = "---",
1985
  ocved_n = "---", n_ged = "---",
 
1986
  sub_incidents = " ", sub_fatalities = " ",
1987
  mun_df = NULL,
1988
  dl_list = NULL,
@@ -2213,6 +2404,11 @@ server <- function(input, output, session) {
2213
  rv$n_desap <- fmt_n(mun_agg$desap_n)
2214
  rv$n_pam <- fmt_n(mun_agg$pam_n)
2215
 
 
 
 
 
 
2216
  rv$mun_df <- tryCatch(DBI::dbGetQuery(con, web_sql("mun_table")),
2217
  error = function(e) data.frame())
2218
  }
@@ -2283,6 +2479,23 @@ server <- function(input, output, session) {
2283
  rv$n_pam <- fmt_n(sum(mun_df$pam_n, na.rm = TRUE))
2284
  rv$mun_df <- mun_df
2285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2286
  rv$dl_list <- list(
2287
  migrants = migrants_df, graves = graves_df,
2288
  infra = infra_df, ocved = ocved_df,
@@ -2936,6 +3149,8 @@ server <- function(input, output, session) {
2936
  output$n_mun <- renderText(rv$n_mun)
2937
  output$n_desap <- renderText(rv$n_desap)
2938
  output$n_pam <- renderText(rv$n_pam)
 
 
2939
  output$sub_incidents <- renderText(rv$sub_incidents)
2940
  output$sub_fatalities <- renderText(rv$sub_fatalities)
2941
 
@@ -2977,7 +3192,7 @@ server <- function(input, output, session) {
2977
 
2978
  output$temporal_plot <- renderPlot({
2979
  req(con)
2980
- # Try incident_date_ts (DATE, from parquet) then fall back to incident_date
2981
  yr_expr_ts <- "CAST(date_part('year', incident_date_ts) AS INTEGER)"
2982
  yr_expr_raw <- "CAST(date_part('year', CAST(incident_date AS TIMESTAMP)) AS INTEGER)"
2983
  df_all <- tryCatch(
@@ -2995,17 +3210,27 @@ server <- function(input, output, session) {
2995
  yr_expr = yr_expr_ts, poly_wkt = filter_wkt)), error = function(e) NULL)
2996
  } else NULL
2997
 
2998
- # Match the migrants point colour (COL_YEL) for visual consistency
2999
- COL_LINE <- COL_YEL
3000
- p <- ggplot(df_all, aes(x = yr, y = n)) +
3001
- geom_area(fill = COL_LINE, alpha = 0.08, color = NA) +
3002
- geom_line(color = COL_LINE, linewidth = 0.7, alpha = 0.45)
 
 
 
 
 
3003
  if (!is.null(df_sel) && nrow(df_sel) > 0) {
3004
  p <- p +
3005
- geom_line(data = df_sel, aes(x = yr, y = n),
3006
- color = COL_LINE, linewidth = 1.1) +
3007
- geom_point(data = df_sel, aes(x = yr, y = n),
3008
- color = COL_LINE, size = 1.6, shape = 21,
 
 
 
 
 
3009
  fill = "white", stroke = 0.7)
3010
  }
3011
  p + theme_minimal(base_size = 8) +
@@ -3065,6 +3290,102 @@ server <- function(input, output, session) {
3065
  output$temporal_ocved <- make_temporal_yr("ocved_events", "year", COL_AMBER)
3066
  output$temporal_ged <- make_temporal_yr("ged_events", "year", COL_LIGHT_BLUE)
3067
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3068
  output$dl_xlsx <- downloadHandler(
3069
  filename = function()
3070
  glue("depth_selection_{format(Sys.time(), '%Y%m%d_%H%M%S')}.xlsx"),
 
321
  "SELECT wkt FROM state_polygons WHERE CVE_ENT = '{cve_ent}' LIMIT 1",
322
 
323
  temporal_mig_all =
324
+ "SELECT {yr_expr} AS yr,
325
+ COUNT(*) AS n,
326
+ COALESCE(SUM(CAST(n_dead AS INTEGER)), 0) AS n_dead
327
+ FROM migrants
328
  WHERE incident_date_ts IS NOT NULL
329
  GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
330
 
331
  temporal_mig_sel =
332
+ "SELECT {yr_expr} AS yr,
333
+ COUNT(*) AS n,
334
+ COALESCE(SUM(CAST(n_dead AS INTEGER)), 0) AS n_dead
335
+ FROM migrants
336
  WHERE incident_date_ts IS NOT NULL
337
  AND lon IS NOT NULL AND lat IS NOT NULL
338
  AND ST_Within(ST_Point(lon, lat), ST_GeomFromText('{poly_wkt}'))
339
  GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
340
 
341
+ temporal_pam_all =
342
+ "SELECT CAST(date_part('year', date) AS INTEGER) AS yr,
343
+ COALESCE(SUM(pam_n), 0) AS n
344
+ FROM dim_pam_mun
345
+ WHERE date IS NOT NULL
346
+ GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
347
+
348
+ temporal_pam_sel =
349
+ "SELECT CAST(date_part('year', date) AS INTEGER) AS yr,
350
+ COALESCE(SUM(pam_n), 0) AS n
351
+ FROM dim_pam_mun
352
+ WHERE date IS NOT NULL
353
+ AND CVEGEO IN ({cvegeo_list})
354
+ GROUP BY yr HAVING yr > 2000 AND yr <= 2030 ORDER BY yr",
355
+
356
+ donut_giim =
357
+ "SELECT COALESCE(giim_mun, 'No aplica') AS cat,
358
+ COUNT(*) AS n
359
+ FROM dim_remesas_mun
360
+ GROUP BY cat ORDER BY n DESC",
361
+
362
+ donut_infra =
363
+ "SELECT
364
+ CASE
365
+ WHEN category = 'Shelter' THEN 'Shelter'
366
+ WHEN LOWER(category) IN ('food','restaurant','lodging') THEN 'Food & Lodging'
367
+ WHEN LOWER(category) IN ('government','embassy') THEN 'Government'
368
+ WHEN category = 'Legal/NGO' OR LOWER(category) = 'charity' THEN 'Legal & Aid'
369
+ WHEN LOWER(category) = 'police' THEN 'Police'
370
+ WHEN LOWER(category) IN ('hospital','medical') THEN 'Health'
371
+ WHEN LOWER(category) IN ('religious','place-of-worship') THEN 'Religious'
372
+ ELSE 'Other'
373
+ END AS cat,
374
+ COUNT(*) AS n
375
+ FROM support_infrastructure
376
+ WHERE category IS NOT NULL
377
+ GROUP BY cat ORDER BY n DESC",
378
+
379
+ fosas_nat =
380
+ "SELECT COALESCE(SUM(fosas_total), 0) AS fosas_n,
381
+ COALESCE(SUM(cuerpos_total), 0) AS cuerpos_n
382
+ FROM dim_fosas_mun",
383
+
384
+ fosas_sel =
385
+ "SELECT COALESCE(SUM(fosas_total), 0) AS fosas_n,
386
+ COALESCE(SUM(cuerpos_total), 0) AS cuerpos_n
387
+ FROM dim_fosas_mun
388
+ WHERE CVEGEO IN ({cvegeo_list})",
389
+
390
  temporal_yr_all =
391
  "SELECT {yr_expr} AS yr, COUNT(*) AS n FROM {tbl}
392
  WHERE {yr_col} IS NOT NULL
 
603
  #######################################################################################
604
 
605
  .NOMINATIM_CACHE <- new.env(parent = emptyenv())
606
+ .NOMINATIM_UA <- "DEPTH-RiskMap/0.001"
607
 
608
  nominatim_search <- function(q, limit = 6, country = "mx") {
609
  q <- trimws(q %||% "")
 
750
  legend = list(type = "ramp", pal = "desap", unit = " /100k")),
751
  list(kind = "layer", value = "fosas-choro",
752
  label = "Clandestine burial pits by municipality",
753
+ tooltip = "Number of clandestine burial pits (fosas) and bodies found per municipality. Source: Plataforma Fosas Clandestinas.",
754
  legend = list(type = "ramp", pal = "fosas", unit = " pits")),
755
  list(kind = "radios", group = "fosas"),
756
  list(kind = "layer", value = "graves-pts",
 
830
  )),
831
  list(title = "Enforced Disappearances (RNPDNO)", kind = "stat",
832
  stats = list(list(id = "n_desap", label = "Registered disappearances", color = COL_RISK))),
833
+ # Side-by-side burial pair: graves (points) + fosas (municipality sums)
834
+ list(title = "Burial Sites", kind = "stat", half = TRUE,
835
  stats = list(list(id = "n_graves", label = "Clandestine burial sites", color = COL_RED))),
836
+ list(title = "Burial Pits (Fosas)", kind = "stat", half = TRUE,
837
+ stats = list(
838
+ list(id = "n_fosas", label = "Clandestine pits", color = COL_RED),
839
+ list(id = "n_cuerpos", label = "Bodies recovered", color = COL_AMBER)
840
+ )),
841
  list(title = "Organised Crime Violence (OCVED 2.0)", kind = "stat", chart = "temporal_ocved",
842
  stats = list(list(id = "ocved_n", label = "Recorded violence events", color = COL_AMBER))),
843
  list(title = "Armed Conflict Events (UCDP-GED)", kind = "stat", chart = "temporal_ged",
 
845
  list(title = "Criminal Incidence by Municipality", kind = "munlist")
846
  )),
847
  list(id = "exposure", label = "Exposure", open = FALSE, cards = list(
848
+ list(title = "Irregular Migration Events (PAM)", kind = "stat", chart = "temporal_pam",
849
+ stats = list(list(id = "n_pam", label = "Recorded irregular migration events", color = COL_COUNT))),
850
+ list(title = "Migration Intensity (GIIM)", kind = "donut", chart = "donut_giim",
851
+ note = "Share of Mexican municipalities by intensity of migration to the USA.")
852
  )),
853
  list(id = "vulnerability", label = "Vulnerability", open = FALSE, cards = list(
854
+ list(title = "Migrant Support Facilities", kind = "stat", chart = "donut_infra",
855
+ stats = list(list(id = "n_facilities", label = "Active support facilities", color = COL_GREEN)),
856
+ note = "Composition of support facilities by type (national).")
857
  ))
858
  )
859
  )
 
903
  Shiny.setInputValue('grid_nivel', '6', { priority: 'event' });
904
  }, 300);
905
 
906
+ // Hook up the native draw buttons so our custom ones can mirror their
907
+ // active state. Retries until mapgl has mounted the draw control.
908
  var navTries = 0;
909
  var navTimer = setInterval(function() {
910
  navTries++;
911
+ if (watchNativeDrawButtons() || navTries >= 40) {
912
+ clearInterval(navTimer);
913
+ syncDrawButtonStates();
914
+ }
915
  }, 100);
916
  });
917
 
 
1104
  }
1105
  }
1106
 
1107
+ // -- Side panel toggle (sigma / network / sparkles tab rail) ---------------
1108
  function togglePanel(name) {
1109
  var panel = document.getElementById('panel-' + name);
1110
  var tab = document.getElementById('tab-' + name);
 
1114
  panel.classList.toggle('is-collapsed', !willOpen);
1115
  tab.classList.toggle('is-active', willOpen);
1116
  tab.setAttribute('aria-selected', String(willOpen));
1117
+ // Stack collapses to 0 width when ALL three panels are closed
1118
+ var anyOpen = ['tmri', 'risk', 'assist'].some(function(k) {
1119
+ var p = document.getElementById('panel-' + k);
1120
+ return p && !p.classList.contains('is-collapsed');
1121
+ });
1122
  if (stack) stack.classList.toggle('is-empty', !anyOpen);
1123
+ // Re-render Lucide icons in case the panel mounted icons just now
1124
  if (willOpen && window.lucide) {
1125
  setTimeout(function() { lucide.createIcons(); }, 30);
1126
  }
 
1147
  } catch(e) {}
1148
  }
1149
 
1150
+ // -- Custom draw buttons: proxy-click the (hidden) native mapgl buttons ----
1151
+ // mapgl mounts its native MapboxDraw buttons inside the map control group.
1152
+ // We hide them via CSS but keep them in the DOM so .click() still triggers
1153
+ // the underlying draw.changeMode() / draw mode behaviour. Our Lucide-icon
1154
+ // buttons in #nav-tools-select call drawMode() which forwards the click.
1155
+ function drawMode(modeKey) {
1156
+ var sel = '.mapbox-gl-draw_' + modeKey;
1157
+ var nativeBtn = document.querySelector(sel);
1158
+ if (!nativeBtn) {
1159
+ // Native draw control not yet mounted -- retry briefly
1160
+ setTimeout(function() {
1161
+ var b = document.querySelector(sel);
1162
+ if (b) { b.click(); setTimeout(syncDrawButtonStates, 60); }
1163
+ }, 200);
1164
+ return;
1165
+ }
1166
+ nativeBtn.click();
1167
+ // Sync our custom buttons' .active state from the natives after click
1168
+ setTimeout(syncDrawButtonStates, 60);
1169
+ }
1170
+
1171
+ function syncDrawButtonStates() {
1172
+ ['polygon', 'rectangle', 'radius', 'line_string'].forEach(function(mode) {
1173
+ var native = document.querySelector('.mapbox-gl-draw_' + mode);
1174
+ var custom = document.querySelector('[data-draw=\"' + mode + '\"]');
1175
+ if (native && custom) {
1176
+ custom.classList.toggle('active', native.classList.contains('active'));
1177
+ }
1178
+ });
1179
+ }
1180
+
1181
+ // Watch native draw buttons so our custom state mirrors mode changes
1182
+ // triggered outside our buttons (e.g. when mapgl reverts to simple_select
1183
+ // after a polygon is completed).
1184
+ function watchNativeDrawButtons() {
1185
+ var natives = document.querySelectorAll(
1186
  '.mapbox-gl-draw_polygon, .mapbox-gl-draw_rectangle, .mapbox-gl-draw_radius, .mapbox-gl-draw_line_string'
1187
  );
1188
+ if (!natives.length) return false;
1189
+ var obs = new MutationObserver(syncDrawButtonStates);
1190
+ natives.forEach(function(b) {
1191
+ obs.observe(b, { attributes: true, attributeFilter: ['class'] });
 
 
 
 
 
 
 
 
 
 
1192
  });
 
 
 
 
 
 
 
 
 
1193
  return true;
1194
  }
1195
 
 
1577
  )
1578
  }
1579
 
1580
+ ind_card <- function(title, body, chart_id = NULL, note = NULL, soon = FALSE,
1581
+ half = FALSE, chart_height = "55px") {
1582
+ cls <- paste("ind-card", if (isTRUE(half)) "ind-card--half" else "")
1583
+ div(class = cls,
1584
  div(class = "ind-card-hdr",
1585
  span(class = "ind-card-title", title),
1586
  if (soon) tags$span(class = "acc-coming-tag", "Soon")
1587
  ),
1588
+ if (!is.null(body)) div(class = "ind-card-body", body),
 
 
1589
  if (!is.null(chart_id))
1590
  div(class = "ind-card-chart",
1591
+ plotOutput(chart_id, width = "100%", height = chart_height)),
1592
+ if (!is.null(note))
1593
+ div(class = "ind-card-note", note)
1594
  )
1595
  }
1596
 
 
1670
  uiOutput("mun_list")),
1671
  stat = tagList(
1672
  lapply(card$stats, function(s)
1673
+ dp_stat(s$id, s$label, s$color %||% "#1e293b"))),
1674
+ donut = NULL # donut renders entirely via plotOutput(chart_id)
1675
  )
1676
+ # Donut cards are taller (the chart IS the body); other cards keep the
1677
+ # compact 55px sparkline. Half cards have slightly less chrome.
1678
+ chart_h <- if (identical(card$kind, "donut")) "180px" else "55px"
1679
+ ind_card(card$title, body,
1680
+ chart_id = card$chart,
1681
+ note = card$note,
1682
+ half = isTRUE(card$half),
1683
+ chart_height = chart_h)
1684
  }
1685
 
1686
  render_panel_groups <- function() {
 
1742
  div(class = "nav-divider"),
1743
 
1744
  # Selection / drawing tool group.
1745
+ # Custom Lucide buttons proxy-click the (hidden) native mapgl draw buttons
1746
+ # via drawMode() in JS, so the look is consistent with the rest of the nav.
1747
  div(class = "nav-group", id = "nav-tools-select",
1748
+ tags$button(id = "btn_draw_polygon", class = "nav-btn",
1749
+ type = "button",
1750
+ `data-draw` = "polygon",
1751
+ `aria-label` = "Lasso / freehand polygon",
1752
+ title = "Lasso (freehand polygon)",
1753
+ onclick = "drawMode('polygon')",
1754
+ HTML('<i data-lucide="lasso" style="width:16px;height:16px;stroke-width:2"></i>')),
1755
+ tags$button(id = "btn_draw_rectangle", class = "nav-btn",
1756
+ type = "button",
1757
+ `data-draw` = "rectangle",
1758
+ `aria-label` = "Rectangle selection",
1759
+ title = "Rectangle",
1760
+ onclick = "drawMode('rectangle')",
1761
+ HTML('<i data-lucide="square-dashed" style="width:16px;height:16px;stroke-width:2"></i>')),
1762
+ tags$button(id = "btn_draw_radius", class = "nav-btn",
1763
+ type = "button",
1764
+ `data-draw` = "radius",
1765
+ `aria-label` = "Circle / radius selection",
1766
+ title = "Circle (radius)",
1767
+ onclick = "drawMode('radius')",
1768
+ HTML('<i data-lucide="circle-dashed" style="width:16px;height:16px;stroke-width:2"></i>')),
1769
+ tags$button(id = "btn_draw_line", class = "nav-btn",
1770
+ type = "button",
1771
+ `data-draw` = "line_string",
1772
+ `aria-label` = "Line selection",
1773
+ title = "Line",
1774
+ onclick = "drawMode('line_string')",
1775
+ HTML('<i data-lucide="spline" style="width:16px;height:16px;stroke-width:2"></i>')),
1776
  tags$button(id = "btn_mode_click", class = "nav-btn",
1777
  type = "button",
1778
  `aria-label` = "Click a municipality to analyse it",
 
1980
  )
1981
  ),
1982
 
1983
+ # ---- Risk Index Adjustments panel (collapsed by default) ------------
1984
+ tags$section(class = "side-panel side-panel--risk is-collapsed",
1985
+ id = "panel-risk",
1986
+ role = "region",
1987
+ `aria-label` = "Risk Index Adjustments",
1988
+ div(class = "side-panel-header",
1989
+ tags$span(class = "side-panel-icon",
1990
+ HTML('<i data-lucide="network" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>')),
1991
+ tags$h2(class = "side-panel-title", "Risk Adjustments"),
1992
+ tags$span(class = "side-panel-tag", "Idea"),
1993
+ tags$button(class = "side-panel-collapse",
1994
+ type = "button",
1995
+ onclick = "togglePanel('risk')",
1996
+ `aria-label` = "Collapse Risk Adjustments",
1997
+ title = "Collapse",
1998
+ HTML('<i data-lucide="panel-right-close" style="width:16px;height:16px;stroke-width:2" aria-hidden="true"></i>'))
1999
+ ),
2000
+ div(class = "side-panel-body risk-body",
2001
+ div(class = "assist-card",
2002
+ tags$p(class = "assist-intro",
2003
+ "A long-shot idea -- ", tags$em("may not be tractable."),
2004
+ " The goal is to let researchers re-weight each component of the Transit Migration Risk Index and watch the composite score recompute over the map."
2005
+ ),
2006
+ tags$div(class = "assist-status",
2007
+ HTML('<i data-lucide="flask-conical" style="width:12px;height:12px;stroke-width:2;color:var(--c-text-mid)" aria-hidden="true"></i>'),
2008
+ tags$span("Research preview -- weighting + composition under exploration")
2009
+ ),
2010
+ tags$div(class = "risk-weights",
2011
+ tags$div(class = "risk-weight",
2012
+ tags$label(`for` = "rw-hazards",
2013
+ span(class = "risk-weight-label", "Hazards"),
2014
+ span(class = "risk-weight-val", "0.50")),
2015
+ tags$input(type = "range", id = "rw-hazards", min = "0", max = "1",
2016
+ step = "0.05", value = "0.50",
2017
+ `aria-label` = "Hazards weight",
2018
+ disabled = "disabled")
2019
+ ),
2020
+ tags$div(class = "risk-weight",
2021
+ tags$label(`for` = "rw-exposure",
2022
+ span(class = "risk-weight-label", "Exposure"),
2023
+ span(class = "risk-weight-val", "0.30")),
2024
+ tags$input(type = "range", id = "rw-exposure", min = "0", max = "1",
2025
+ step = "0.05", value = "0.30",
2026
+ `aria-label` = "Exposure weight",
2027
+ disabled = "disabled")
2028
+ ),
2029
+ tags$div(class = "risk-weight",
2030
+ tags$label(`for` = "rw-vuln",
2031
+ span(class = "risk-weight-label", "Vulnerability"),
2032
+ span(class = "risk-weight-val", "0.20")),
2033
+ tags$input(type = "range", id = "rw-vuln", min = "0", max = "1",
2034
+ step = "0.05", value = "0.20",
2035
+ `aria-label` = "Vulnerability weight",
2036
+ disabled = "disabled")
2037
+ )
2038
+ ),
2039
+ tags$div(class = "assist-input-mock", role = "presentation",
2040
+ HTML('<i data-lucide="play" style="width:14px;height:14px;stroke-width:2;color:var(--c-text-dim)" aria-hidden="true"></i>'),
2041
+ tags$span("Recompute index"),
2042
+ tags$span(class = "assist-soon-tag", "Soon")
2043
+ )
2044
+ )
2045
+ )
2046
+ ),
2047
+
2048
  # ---- Assistance panel (collapsed by default) -----------------------
2049
  tags$section(class = "side-panel side-panel--assist is-collapsed",
2050
  id = "panel-assist",
 
2102
  `aria-label` = "Toggle Transit Migration Risk Index",
2103
  onclick = "togglePanel('tmri')",
2104
  HTML('<i data-lucide="sigma" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
2105
+ tags$span(class = "side-rail-tab-label", "Summary")),
2106
+ tags$button(class = "side-rail-tab",
2107
+ id = "tab-risk",
2108
+ type = "button",
2109
+ `data-panel` = "risk",
2110
+ role = "tab",
2111
+ `aria-controls` = "panel-risk",
2112
+ `aria-selected` = "false",
2113
+ `aria-label` = "Toggle Risk Index Adjustments",
2114
+ onclick = "togglePanel('risk')",
2115
+ HTML('<i data-lucide="network" style="width:18px;height:18px;stroke-width:2" aria-hidden="true"></i>'),
2116
+ tags$span(class = "side-rail-tab-label", "Adjust")),
2117
  tags$button(class = "side-rail-tab",
2118
  id = "tab-assist",
2119
  type = "button",
 
2173
  n_events = "---", n_mun = "---",
2174
  n_desap = "---", n_pam = "---",
2175
  ocved_n = "---", n_ged = "---",
2176
+ n_fosas = "---", n_cuerpos = "---",
2177
  sub_incidents = " ", sub_fatalities = " ",
2178
  mun_df = NULL,
2179
  dl_list = NULL,
 
2404
  rv$n_desap <- fmt_n(mun_agg$desap_n)
2405
  rv$n_pam <- fmt_n(mun_agg$pam_n)
2406
 
2407
+ fosas <- tryCatch(DBI::dbGetQuery(con, web_sql("fosas_nat")),
2408
+ error = function(e) data.frame(fosas_n = 0, cuerpos_n = 0))
2409
+ rv$n_fosas <- fmt_n(fosas$fosas_n)
2410
+ rv$n_cuerpos <- fmt_n(fosas$cuerpos_n)
2411
+
2412
  rv$mun_df <- tryCatch(DBI::dbGetQuery(con, web_sql("mun_table")),
2413
  error = function(e) data.frame())
2414
  }
 
2479
  rv$n_pam <- fmt_n(sum(mun_df$pam_n, na.rm = TRUE))
2480
  rv$mun_df <- mun_df
2481
 
2482
+ # Fosas / cuerpos sums for the selected municipalities (or zero if none).
2483
+ fosas_sum <- list(fosas_n = 0, cuerpos_n = 0)
2484
+ if (nrow(mun_df) > 0 && !is.null(mun_df$CVEGEO)) {
2485
+ cvegeos <- unique(mun_df$CVEGEO[nzchar(mun_df$CVEGEO)])
2486
+ if (length(cvegeos) > 0) {
2487
+ vals <- paste0("'", gsub("'", "''", cvegeos), "'", collapse = ",")
2488
+ fs <- tryCatch(DBI::dbGetQuery(con, web_sql("fosas_sel", cvegeo_list = vals)),
2489
+ error = function(e) NULL)
2490
+ if (!is.null(fs) && nrow(fs) == 1) {
2491
+ fosas_sum$fosas_n <- as.numeric(fs$fosas_n)
2492
+ fosas_sum$cuerpos_n <- as.numeric(fs$cuerpos_n)
2493
+ }
2494
+ }
2495
+ }
2496
+ rv$n_fosas <- fmt_n(fosas_sum$fosas_n)
2497
+ rv$n_cuerpos <- fmt_n(fosas_sum$cuerpos_n)
2498
+
2499
  rv$dl_list <- list(
2500
  migrants = migrants_df, graves = graves_df,
2501
  infra = infra_df, ocved = ocved_df,
 
3149
  output$n_mun <- renderText(rv$n_mun)
3150
  output$n_desap <- renderText(rv$n_desap)
3151
  output$n_pam <- renderText(rv$n_pam)
3152
+ output$n_fosas <- renderText(rv$n_fosas)
3153
+ output$n_cuerpos <- renderText(rv$n_cuerpos)
3154
  output$sub_incidents <- renderText(rv$sub_incidents)
3155
  output$sub_fatalities <- renderText(rv$sub_fatalities)
3156
 
 
3192
 
3193
  output$temporal_plot <- renderPlot({
3194
  req(con)
3195
+ # IOM temporal: incidents (COL_YEL) + confirmed dead (COL_RED) overlay
3196
  yr_expr_ts <- "CAST(date_part('year', incident_date_ts) AS INTEGER)"
3197
  yr_expr_raw <- "CAST(date_part('year', CAST(incident_date AS TIMESTAMP)) AS INTEGER)"
3198
  df_all <- tryCatch(
 
3210
  yr_expr = yr_expr_ts, poly_wkt = filter_wkt)), error = function(e) NULL)
3211
  } else NULL
3212
 
3213
+ COL_INC <- COL_YEL # incidents
3214
+ COL_DEAD <- COL_RED # confirmed dead
3215
+
3216
+ p <- ggplot(df_all, aes(x = yr)) +
3217
+ # Incidents area + line
3218
+ geom_area(aes(y = n), fill = COL_INC, alpha = 0.10, color = NA) +
3219
+ geom_line(aes(y = n), color = COL_INC, linewidth = 0.7, alpha = 0.55) +
3220
+ # Confirmed dead overlay
3221
+ geom_line(aes(y = n_dead), color = COL_DEAD, linewidth = 0.7,
3222
+ alpha = 0.6, linetype = "22")
3223
  if (!is.null(df_sel) && nrow(df_sel) > 0) {
3224
  p <- p +
3225
+ geom_line(data = df_sel, aes(y = n),
3226
+ color = COL_INC, linewidth = 1.1) +
3227
+ geom_point(data = df_sel, aes(y = n),
3228
+ color = COL_INC, size = 1.5, shape = 21,
3229
+ fill = "white", stroke = 0.7) +
3230
+ geom_line(data = df_sel, aes(y = n_dead),
3231
+ color = COL_DEAD, linewidth = 1.0, linetype = "22") +
3232
+ geom_point(data = df_sel, aes(y = n_dead),
3233
+ color = COL_DEAD, size = 1.3, shape = 21,
3234
  fill = "white", stroke = 0.7)
3235
  }
3236
  p + theme_minimal(base_size = 8) +
 
3290
  output$temporal_ocved <- make_temporal_yr("ocved_events", "year", COL_AMBER)
3291
  output$temporal_ged <- make_temporal_yr("ged_events", "year", COL_LIGHT_BLUE)
3292
 
3293
+ # --- PAM temporal plot (irregular migration events over time) ---------------
3294
+ # dim_pam_mun is municipality-aggregated by date. National = SUM over all
3295
+ # munis; selection = SUM over munis in rv$mun_df (CVEGEO list).
3296
+ output$temporal_pam <- renderPlot({
3297
+ req(con)
3298
+ df_all <- tryCatch(DBI::dbGetQuery(con, web_sql("temporal_pam_all")),
3299
+ error = function(e) { message("pam temporal: ", e$message); NULL })
3300
+ if (is.null(df_all) || nrow(df_all) == 0) return(NULL)
3301
+
3302
+ df_sel <- NULL
3303
+ if (!rv$is_national && !is.null(rv$mun_df) && nrow(rv$mun_df) > 0) {
3304
+ cvegeos <- unique(rv$mun_df$CVEGEO)
3305
+ cvegeos <- cvegeos[nzchar(cvegeos)]
3306
+ if (length(cvegeos) > 0) {
3307
+ # Quote and join into a SQL IN list (CVEGEOs are 5-char digit codes,
3308
+ # safe to interpolate after the nzchar/unique pass above).
3309
+ vals <- paste0("'", gsub("'", "''", cvegeos), "'", collapse = ",")
3310
+ df_sel <- tryCatch(
3311
+ DBI::dbGetQuery(con, web_sql("temporal_pam_sel", cvegeo_list = vals)),
3312
+ error = function(e) NULL
3313
+ )
3314
+ }
3315
+ }
3316
+
3317
+ COL_LINE <- COL_COUNT
3318
+ p <- ggplot(df_all, aes(x = yr, y = n)) +
3319
+ geom_area(fill = COL_LINE, alpha = 0.08, color = NA) +
3320
+ geom_line(color = COL_LINE, linewidth = 0.7, alpha = 0.45)
3321
+ if (!is.null(df_sel) && nrow(df_sel) > 0) {
3322
+ p <- p +
3323
+ geom_line(data = df_sel, aes(x = yr, y = n),
3324
+ color = COL_LINE, linewidth = 1.1) +
3325
+ geom_point(data = df_sel, aes(x = yr, y = n),
3326
+ color = COL_LINE, size = 1.6, shape = 21,
3327
+ fill = "white", stroke = 0.7)
3328
+ }
3329
+ p + theme_minimal(base_size = 8) +
3330
+ theme(
3331
+ plot.background = element_rect(fill = "transparent", color = NA),
3332
+ panel.background = element_rect(fill = "transparent", color = NA),
3333
+ panel.grid = element_blank(),
3334
+ panel.grid.major.y = element_line(color = "#e2e8f0", linewidth = 0.3),
3335
+ axis.title = element_blank(),
3336
+ axis.text.x = element_text(size = 7, color = "#64748b"),
3337
+ axis.text.y = element_blank(),
3338
+ axis.ticks = element_blank(),
3339
+ plot.margin = margin(1, 4, 1, 4)
3340
+ )
3341
+ }, bg = "transparent", res = 96)
3342
+
3343
+ # --- Donut helper -----------------------------------------------------------
3344
+ # Renders a transparent-background donut with category-coloured slices.
3345
+ # `df` columns: cat (chr) and n (numeric). `palette` is a named vector
3346
+ # mapping category -> hex.
3347
+ donut_plot <- function(df, palette, center_text = NULL) {
3348
+ if (is.null(df) || nrow(df) == 0) return(NULL)
3349
+ df$cat <- factor(df$cat, levels = names(palette))
3350
+ df <- df[!is.na(df$cat) & df$n > 0, , drop = FALSE]
3351
+ if (nrow(df) == 0) return(NULL)
3352
+ total <- sum(df$n)
3353
+ p <- ggplot(df, aes(x = 2, y = n, fill = cat)) +
3354
+ geom_col(width = 1, color = "#ffffff", linewidth = 0.4) +
3355
+ coord_polar(theta = "y", start = 0) +
3356
+ scale_fill_manual(values = palette, drop = FALSE, guide = "none") +
3357
+ xlim(0.4, 2.5) +
3358
+ theme_void() +
3359
+ theme(plot.background = element_rect(fill = "transparent", color = NA),
3360
+ panel.background = element_rect(fill = "transparent", color = NA),
3361
+ plot.margin = margin(2, 2, 2, 2))
3362
+ if (!is.null(center_text)) {
3363
+ p <- p + annotate("text", x = 0.4, y = 0,
3364
+ label = center_text,
3365
+ family = "Inter", fontface = "bold",
3366
+ size = 4, color = "#0f172a")
3367
+ }
3368
+ p
3369
+ }
3370
+
3371
+ # --- GIIM donut (municipalities by migration-intensity category) -----------
3372
+ output$donut_giim <- renderPlot({
3373
+ req(con)
3374
+ df <- tryCatch(DBI::dbGetQuery(con, web_sql("donut_giim")),
3375
+ error = function(e) NULL)
3376
+ donut_plot(df, GIIM_COLORS,
3377
+ center_text = if (!is.null(df)) format(sum(df$n), big.mark = ",") else NULL)
3378
+ }, bg = "transparent", res = 96)
3379
+
3380
+ # --- Infra donut (facilities by INFRA_CATS bucket) -------------------------
3381
+ output$donut_infra <- renderPlot({
3382
+ req(con)
3383
+ df <- tryCatch(DBI::dbGetQuery(con, web_sql("donut_infra")),
3384
+ error = function(e) NULL)
3385
+ donut_plot(df, INFRA_CATS,
3386
+ center_text = if (!is.null(df)) format(sum(df$n), big.mark = ",") else NULL)
3387
+ }, bg = "transparent", res = 96)
3388
+
3389
  output$dl_xlsx <- downloadHandler(
3390
  filename = function()
3391
  glue("depth_selection_{format(Sys.time(), '%Y%m%d_%H%M%S')}.xlsx"),
styles.css CHANGED
@@ -1187,15 +1187,25 @@
1187
  display: none;
1188
  }
1189
 
1190
- /* -- Hide MapLibre Draw buttons not needed -------------------------------- */
1191
- /* polygon (lasso), rectangle, radius and line_string are all relocated into */
1192
- /* the top nav-bar by relocateDrawButtonsToNav(). Hide only point/trash/ */
1193
- /* combine/uncombine which are not useful here. */
 
 
 
 
1194
  button.mapbox-gl-draw_point,
1195
  button.mapbox-gl-draw_trash,
1196
  button.mapbox-gl-draw_combine,
1197
  button.mapbox-gl-draw_uncombine { display: none !important; }
1198
 
 
 
 
 
 
 
1199
  /* (draw-extra-ctrl position now set in the overrides section below) */
1200
 
1201
  /* (Viewport coords moved into .nav-coords inside the top nav-bar) */
@@ -1314,8 +1324,8 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
1314
 
1315
  /* ---- Brand ---- */
1316
  .nav-brand {
1317
- font-size: 13px;
1318
- font-weight: 700;
1319
  letter-spacing: 0.16em;
1320
  text-transform: uppercase;
1321
  color: var(--c-text);
@@ -1327,8 +1337,8 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
1327
 
1328
  .nav-brand-dot {
1329
  display: inline-block;
1330
- width: 6px;
1331
- height: 6px;
1332
  border-radius: 50%;
1333
  background: var(--c-accent);
1334
  margin-right: 8px;
@@ -1648,25 +1658,25 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
1648
 
1649
  .panel-stack {
1650
  display: flex;
1651
- flex-direction: column;
1652
- width: 384px;
1653
- border-right: 1px solid var(--c-glass-stroke);
1654
  background: oklch(99% 0.002 250);
1655
  box-shadow: inset 1px 0 0 oklch(100% 0 0 / 0.6);
1656
  overflow: hidden;
1657
- transition: width var(--t-slow) var(--ease-spring),
1658
- border-color var(--t-base) var(--ease-out);
1659
  }
1660
 
1661
  .panel-stack.is-empty {
1662
- width: 0;
1663
  border-right-color: transparent;
1664
  }
1665
 
1666
  .side-panel {
1667
  display: flex;
1668
  flex-direction: column;
1669
- flex: 1 1 auto;
 
 
 
1670
  min-height: 0;
1671
  overflow: hidden;
1672
  }
@@ -1674,7 +1684,7 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
1674
  .side-panel.is-collapsed { display: none; }
1675
 
1676
  .side-panel + .side-panel {
1677
- border-top: 1px solid var(--c-glass-stroke);
1678
  }
1679
 
1680
  .side-panel-header {
@@ -2146,3 +2156,111 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
2146
  text-decoration: line-through;
2147
  color: var(--c-text-dim);
2148
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1187
  display: none;
1188
  }
1189
 
1190
+ /* -- Hide ALL MapLibre Draw native buttons -------------------------------- */
1191
+ /* Custom Lucide-icon buttons in the nav-bar proxy-click these natives via */
1192
+ /* drawMode() in JS. The natives stay in the DOM so click() still triggers */
1193
+ /* the mapgl draw API; they are just visually hidden. */
1194
+ button.mapbox-gl-draw_polygon,
1195
+ button.mapbox-gl-draw_rectangle,
1196
+ button.mapbox-gl-draw_radius,
1197
+ button.mapbox-gl-draw_line_string,
1198
  button.mapbox-gl-draw_point,
1199
  button.mapbox-gl-draw_trash,
1200
  button.mapbox-gl-draw_combine,
1201
  button.mapbox-gl-draw_uncombine { display: none !important; }
1202
 
1203
+ /* Also hide the empty mapgl ctrl-group container once buttons are hidden */
1204
+ .maplibregl-ctrl-top-right .maplibregl-ctrl-group:empty,
1205
+ .maplibregl-ctrl-top-right .maplibregl-ctrl-group:has(button:only-child[style*="none"]) {
1206
+ display: none !important;
1207
+ }
1208
+
1209
  /* (draw-extra-ctrl position now set in the overrides section below) */
1210
 
1211
  /* (Viewport coords moved into .nav-coords inside the top nav-bar) */
 
1324
 
1325
  /* ---- Brand ---- */
1326
  .nav-brand {
1327
+ font-size: 20px;
1328
+ font-weight: 800;
1329
  letter-spacing: 0.16em;
1330
  text-transform: uppercase;
1331
  color: var(--c-text);
 
1337
 
1338
  .nav-brand-dot {
1339
  display: inline-block;
1340
+ width: 7px;
1341
+ height: 7px;
1342
  border-radius: 50%;
1343
  background: var(--c-accent);
1344
  margin-right: 8px;
 
1658
 
1659
  .panel-stack {
1660
  display: flex;
1661
+ flex-direction: row;
 
 
1662
  background: oklch(99% 0.002 250);
1663
  box-shadow: inset 1px 0 0 oklch(100% 0 0 / 0.6);
1664
  overflow: hidden;
1665
+ border-right: 1px solid var(--c-glass-stroke);
1666
+ transition: border-color var(--t-base) var(--ease-out);
1667
  }
1668
 
1669
  .panel-stack.is-empty {
 
1670
  border-right-color: transparent;
1671
  }
1672
 
1673
  .side-panel {
1674
  display: flex;
1675
  flex-direction: column;
1676
+ flex: 1 1 360px;
1677
+ min-width: 320px;
1678
+ max-width: 420px;
1679
+ height: 100%;
1680
  min-height: 0;
1681
  overflow: hidden;
1682
  }
 
1684
  .side-panel.is-collapsed { display: none; }
1685
 
1686
  .side-panel + .side-panel {
1687
+ border-left: 1px solid var(--c-glass-stroke);
1688
  }
1689
 
1690
  .side-panel-header {
 
2156
  text-decoration: line-through;
2157
  color: var(--c-text-dim);
2158
  }
2159
+
2160
+ /* =========================================================================
2161
+ Card grid (full + half-width side-by-side cards)
2162
+ ========================================================================= */
2163
+ .dp-group-body {
2164
+ display: grid;
2165
+ grid-template-columns: 1fr 1fr;
2166
+ gap: var(--space-2);
2167
+ padding: var(--space-1) 0 var(--space-2) 0;
2168
+ }
2169
+
2170
+ /* Default: a card spans both columns (full width) */
2171
+ .ind-card {
2172
+ grid-column: 1 / -1;
2173
+ }
2174
+
2175
+ /* Half-width card -- two consecutive .ind-card--half siblings sit side by side */
2176
+ .ind-card--half {
2177
+ grid-column: auto;
2178
+ display: flex;
2179
+ flex-direction: column;
2180
+ }
2181
+
2182
+ .ind-card--half .ind-card-body { padding: var(--space-2) 10px; }
2183
+
2184
+ /* =========================================================================
2185
+ Risk-weight sliders (placeholder UI inside the network panel)
2186
+ ========================================================================= */
2187
+ .risk-body { padding: var(--space-4); overflow-y: auto; }
2188
+
2189
+ .risk-weights {
2190
+ display: flex;
2191
+ flex-direction: column;
2192
+ gap: var(--space-3);
2193
+ padding-top: var(--space-2);
2194
+ }
2195
+
2196
+ .risk-weight {
2197
+ display: flex;
2198
+ flex-direction: column;
2199
+ gap: 4px;
2200
+ }
2201
+
2202
+ .risk-weight label {
2203
+ display: flex;
2204
+ align-items: baseline;
2205
+ justify-content: space-between;
2206
+ font-family: var(--font-ui);
2207
+ font-size: 11.5px;
2208
+ color: var(--c-text);
2209
+ margin: 0;
2210
+ }
2211
+
2212
+ .risk-weight-label {
2213
+ font-weight: 600;
2214
+ letter-spacing: 0.02em;
2215
+ }
2216
+
2217
+ .risk-weight-val {
2218
+ font-family: var(--font-mono);
2219
+ font-size: 11px;
2220
+ font-variant-numeric: tabular-nums;
2221
+ color: var(--c-text-mid);
2222
+ }
2223
+
2224
+ /* Range input -- subtle, accent track */
2225
+ .risk-weight input[type="range"] {
2226
+ -webkit-appearance: none;
2227
+ appearance: none;
2228
+ width: 100%;
2229
+ height: 4px;
2230
+ background: var(--c-glass-stroke-strong);
2231
+ border-radius: var(--radius-pill);
2232
+ outline: none;
2233
+ cursor: not-allowed; /* disabled in this preview */
2234
+ opacity: 0.85;
2235
+ }
2236
+ .risk-weight input[type="range"]:disabled { opacity: 0.55; }
2237
+
2238
+ .risk-weight input[type="range"]::-webkit-slider-thumb {
2239
+ -webkit-appearance: none;
2240
+ appearance: none;
2241
+ width: 14px;
2242
+ height: 14px;
2243
+ border-radius: 50%;
2244
+ background: var(--c-accent);
2245
+ border: 2px solid #fff;
2246
+ box-shadow: var(--depth-1);
2247
+ cursor: not-allowed;
2248
+ }
2249
+ .risk-weight input[type="range"]::-moz-range-thumb {
2250
+ width: 14px;
2251
+ height: 14px;
2252
+ border-radius: 50%;
2253
+ background: var(--c-accent);
2254
+ border: 2px solid #fff;
2255
+ box-shadow: var(--depth-1);
2256
+ cursor: not-allowed;
2257
+ }
2258
+
2259
+ /* =========================================================================
2260
+ Hide the empty mapgl ctrl-group wrapper (after natives are display:none)
2261
+ ========================================================================= */
2262
+ .maplibregl-ctrl-top-right .maplibregl-ctrl-group {
2263
+ background: transparent !important;
2264
+ box-shadow: none !important;
2265
+ border: none !important;
2266
+ }