edgarodriguez commited on
Commit
ff003c6
·
verified ·
1 Parent(s): 2f22a9f

Upload 2 files

Browse files
Files changed (2) hide show
  1. depth_interactive_map_alpha.R +148 -66
  2. styles.css +69 -13
depth_interactive_map_alpha.R CHANGED
@@ -1185,23 +1185,38 @@ WEB_SPEC <- list(
1185
  list(id = "hazards", label = "Hazards", open = TRUE, items = list(
1186
  list(kind = "layer", value = "crime-choro",
1187
  label = "Criminal incidence by municipality",
1188
- tooltip = "Rate of crimes per 100,000 people in each municipality. Select crime type using the buttons below. Source: Registro Nacional de Incidencia Delictiva (RNID)",
 
 
 
 
1189
  legend = list(type = "ramp", pal = "crime", unit = " /100k",
1190
  labels = "qualitative"),
1191
  checked = TRUE),
1192
  list(kind = "radios", group = "crime"),
1193
  list(kind = "layer", value = "migrants-pts",
1194
  label = "Deaths during migration",
1195
- tooltip = "Each dot is a location where migrants went died, recorded by the International Organization for Migration.",
 
 
 
 
1196
  legend = list(type = "dot", color = COL_YEL, label = "Death location")),
1197
  list(kind = "layer", value = "desap-choro",
1198
  label = "Missing and no located persons per 100k",
1199
- tooltip = "People officially reported as disappeared and no located in each municipality, shown per 100,000 residents (2010 to 2024). Source:
1200
- Registro Nacional de Personas Desaparecidas y No Localizadas (RNPDNO)",
 
 
 
1201
  legend = list(type = "ramp", pal = "desap", unit = " /100k")),
1202
  list(kind = "layer", value = "fosas-choro",
1203
  label = "Clandestine graves by municipality",
1204
- tooltip = "Number of clandestine graves and bodies found per municipality. Source: Plataforma Ciudadana de Fosas (2006-2024)",
 
 
 
 
1205
  legend = list(type = "ramp", pal = "fosas", unit = " pits/bodies",
1206
  labels = "qualitative")),
1207
  list(kind = "radios", group = "fosas"),
@@ -1232,7 +1247,11 @@ WEB_SPEC <- list(
1232
  legend = list(type = "ramp", pal = "pam", unit = " events")),
1233
  list(kind = "layer", value = "remesas-choro",
1234
  label = "Degree of Migration Intensity MEX-USA (GIIM)",
1235
- tooltip = "Index measuring the intensity of migration from Mexico to the United States, from none to very high. Source: CONAPO",
 
 
 
 
1236
  legend = list(type = "cats", pal = "giim", filterable = "remesas")),
1237
  list(kind = "layer", value = "routes-lines",
1238
  label = "Main migration routes through Mexico",
@@ -1256,7 +1275,11 @@ WEB_SPEC <- list(
1256
  legend = list(type = "cats", pal = "infra_cats", filterable = "infra")),
1257
  list(kind = "layer", value = "patp-choro",
1258
  label = "Territorial analysis of poverty",
1259
- tooltip = "Multi-dimensional poverty analysis, including sociodemographics, infrastructure and government. Source: CONEVAL",
 
 
 
 
1260
  legend = list(type = "ramp", pal = "patp", unit = "",
1261
  labels = "qualitative"),
1262
  # Variant legends shown when the matching categorical chip is active.
@@ -1327,9 +1350,9 @@ WEB_SPEC <- list(
1327
  ),
1328
  charts = list(
1329
  list(id = "migrants_treemap_cause", label = "Cause of death",
1330
- height = "60px"),
1331
  list(id = "migrants_treemap_demo", label = "Demographics (children / female / male)",
1332
- height = "44px")
1333
  )),
1334
  list(title = "Missing persons and not located (RNPDNO)", kind = "stat",
1335
  stats = list(list(id = "n_desap", label = "Disappearances & no located", color = COL_RISK))),
@@ -1387,7 +1410,24 @@ LUCIDE_JS <- tags$head(
1387
  setTimeout(initViewport, 800);
1388
  }, 400);
1389
 
1390
- // Tooltip: positioned via JS to escape overflow:hidden containers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1391
  document.addEventListener('mouseover', function(e) {
1392
  var el = e.target.closest('[data-tooltip]');
1393
  if (!el) { hideTooltip(); return; }
@@ -1398,11 +1438,36 @@ LUCIDE_JS <- tags$head(
1398
  tip.className = 'depth-tooltip';
1399
  document.body.appendChild(tip);
1400
  }
1401
- tip.textContent = el.dataset.tooltip;
1402
- var r = el.getBoundingClientRect();
1403
- tip.style.left = (r.right + 8) + 'px';
1404
- tip.style.top = (r.top + r.height / 2) + 'px';
 
 
 
 
 
 
1405
  tip.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1406
  });
1407
  document.addEventListener('mouseout', function(e) {
1408
  if (e.target.closest('[data-tooltip]')) hideTooltip();
@@ -1470,7 +1535,9 @@ LUCIDE_JS <- tags$head(
1470
 
1471
  function hideTooltip() {
1472
  var tip = document.getElementById('depth-tooltip');
1473
- if (tip) tip.style.display = 'none';
 
 
1474
  }
1475
 
1476
  // -- Guided spotlight tour (Driver.js) --------------------------------------
@@ -2380,14 +2447,25 @@ acc_layer <- function(value, label, tooltip = NULL, legend = NULL, checked = FAL
2380
  ),
2381
  span(class = "acc-toggle-text", label)
2382
  ),
2383
- if (!is.null(tooltip))
 
 
 
 
 
 
 
2384
  tags$span(class = "acc-info-wrap",
2385
- `data-tooltip` = tooltip,
2386
- role = "button",
2387
- tabindex = "0",
2388
- `aria-label` = paste0("Info: ", tooltip),
 
 
 
2389
  HTML('<i data-lucide="info" style="width:12px;height:12px;stroke-width:2.5;color:var(--c-text-mid)"></i>')
2390
  )
 
2391
  ),
2392
  if (!is.null(legend)) legend,
2393
  if (!is.null(legend_extras)) legend_extras
@@ -4626,58 +4704,62 @@ server <- function(input, output, session) {
4626
  Female = "#7E4A86", # mauve
4627
  Male = "#3A4B5C") # slate
4628
 
4629
- treemap_layout <- function(df) {
4630
- # df: data.frame(label, value, color); returns df with xmin/xmax/ymin/ymax
 
 
4631
  df <- df[!is.na(df$value) & df$value > 0, , drop = FALSE]
4632
  if (nrow(df) == 0) return(NULL)
4633
  df <- df[order(-df$value), , drop = FALSE]
4634
  total <- sum(df$value)
4635
- # Top row holds the leader; remainder tiles the bottom row.
4636
- leader <- df[1, , drop = FALSE]
4637
- rest <- if (nrow(df) > 1) df[-1, , drop = FALSE] else NULL
4638
- leader$xmin <- 0; leader$xmax <- 1
4639
- leader_share <- leader$value / total
4640
- leader$ymax <- 1; leader$ymin <- 1 - leader_share
4641
- if (!is.null(rest) && nrow(rest) > 0) {
4642
- rest_share <- rest$value / sum(rest$value)
4643
- rest$xmax <- cumsum(rest_share)
4644
- rest$xmin <- c(0, head(rest$xmax, -1))
4645
- rest$ymin <- 0; rest$ymax <- 1 - leader_share
4646
- rbind(leader, rest)
4647
- } else {
4648
- leader$ymin <- 0; leader$ymax <- 1
4649
- leader
4650
- }
4651
  }
4652
 
4653
- # Compact text inside a treemap cell: short label + smaller %, sized so
4654
- # they actually fit a ~70px tall card slot. geom_text `size` is in mm;
4655
- # 2.2mm ~ 6.3pt for the label and 1.8mm ~ 5.1pt for the percentage --
4656
- # smaller than the surrounding card text, which is the point.
4657
- treemap_plot <- function(df, pal) {
4658
- laid <- treemap_layout(df)
 
4659
  if (is.null(laid)) return(NULL)
4660
- total <- sum(laid$value)
4661
- laid$pct <- round(100 * laid$value / total)
4662
- laid$cx <- (laid$xmin + laid$xmax) / 2
4663
- laid$cy <- (laid$ymin + laid$ymax) / 2
4664
- laid$w <- laid$xmax - laid$xmin
4665
- # Truncate long labels so they don't crash the cell border. ~13 chars
4666
- # fits a half-row at the card width; less for narrow cells.
4667
- max_chars <- pmax(6L, pmin(20L, as.integer(laid$w * 26)))
4668
- short <- ifelse(nchar(laid$label) > max_chars,
4669
- paste0(substr(laid$label, 1L, max_chars - 1L), "."),
4670
- laid$label)
4671
- laid$short <- short
 
 
 
4672
  ggplot(laid, aes(xmin = xmin, xmax = xmax,
4673
- ymin = ymin, ymax = ymax,
4674
- fill = label)) +
4675
- geom_rect(color = "#ffffff", linewidth = 0.7) +
4676
- geom_text(aes(x = cx, y = cy + 0.10, label = short),
4677
- size = 2.2, color = "white", family = PLOT_FONT,
4678
- fontface = "bold", lineheight = 0.9) +
4679
- geom_text(aes(x = cx, y = cy - 0.18, label = paste0(pct, "%")),
4680
- size = 1.9, color = "white", family = PLOT_FONT, alpha = 0.85) +
 
 
 
 
 
 
 
 
4681
  scale_fill_manual(values = pal, na.value = "#334155") +
4682
  coord_cartesian(expand = FALSE, xlim = c(0, 1), ylim = c(0, 1)) +
4683
  theme_void() +
@@ -4717,7 +4799,7 @@ server <- function(input, output, session) {
4717
  value = agg$n_dead,
4718
  color = unname(CAUSE_PAL[agg$cause_cat]),
4719
  stringsAsFactors = FALSE)
4720
- treemap_plot(summary_df, CAUSE_PAL)
4721
  }, bg = "transparent", res = 110)
4722
 
4723
  output$migrants_treemap_demo <- renderPlot({
@@ -4731,7 +4813,7 @@ server <- function(input, output, session) {
4731
  color = unname(DEMO_PAL),
4732
  stringsAsFactors = FALSE
4733
  )
4734
- treemap_plot(summary_df, DEMO_PAL)
4735
  }, bg = "transparent", res = 110)
4736
 
4737
  # Shared helper: temporal bar chart by year for point event tables
 
1185
  list(id = "hazards", label = "Hazards", open = TRUE, items = list(
1186
  list(kind = "layer", value = "crime-choro",
1187
  label = "Criminal incidence by municipality",
1188
+ tooltip = list(
1189
+ title = "Criminal incidence",
1190
+ body = "Rate of crimes per 100,000 people in each municipality. Pick a crime type with the chips below the layer to switch the painted column.",
1191
+ meta = "Source: Registro Nacional de Incidencia Delictiva (RNID)"
1192
+ ),
1193
  legend = list(type = "ramp", pal = "crime", unit = " /100k",
1194
  labels = "qualitative"),
1195
  checked = TRUE),
1196
  list(kind = "radios", group = "crime"),
1197
  list(kind = "layer", value = "migrants-pts",
1198
  label = "Deaths during migration",
1199
+ tooltip = list(
1200
+ title = "Migrant deaths",
1201
+ body = "Each dot marks a location where migrants died on their journey. The temporal strip and the cards both report SUM(n_dead), not raw incident counts.",
1202
+ meta = "Source: IOM Missing Migrants Project"
1203
+ ),
1204
  legend = list(type = "dot", color = COL_YEL, label = "Death location")),
1205
  list(kind = "layer", value = "desap-choro",
1206
  label = "Missing and no located persons per 100k",
1207
+ tooltip = list(
1208
+ title = "Disappearances per 100k",
1209
+ body = "People officially reported as disappeared and not located in each municipality, normalised per 100,000 residents (2010-2024). Colour ramp uses per-muni percentile breaks.",
1210
+ meta = "Source: RNPDNO"
1211
+ ),
1212
  legend = list(type = "ramp", pal = "desap", unit = " /100k")),
1213
  list(kind = "layer", value = "fosas-choro",
1214
  label = "Clandestine graves by municipality",
1215
+ tooltip = list(
1216
+ title = "Clandestine graves",
1217
+ body = "Number of clandestine pits and recovered bodies per municipality. Annual data -- use the temporal strip to scope to a year range.",
1218
+ meta = "Source: Plataforma Ciudadana de Fosas (2006-2024)"
1219
+ ),
1220
  legend = list(type = "ramp", pal = "fosas", unit = " pits/bodies",
1221
  labels = "qualitative")),
1222
  list(kind = "radios", group = "fosas"),
 
1247
  legend = list(type = "ramp", pal = "pam", unit = " events")),
1248
  list(kind = "layer", value = "remesas-choro",
1249
  label = "Degree of Migration Intensity MEX-USA (GIIM)",
1250
+ tooltip = list(
1251
+ title = "Migration intensity (GIIM)",
1252
+ body = "Composite index from CONAPO: emigration, return, circular and remittance-receiving households per muni. Click a swatch to hide that GIIM bracket.",
1253
+ meta = "Source: CONAPO Degree of Migration Intensity"
1254
+ ),
1255
  legend = list(type = "cats", pal = "giim", filterable = "remesas")),
1256
  list(kind = "layer", value = "routes-lines",
1257
  label = "Main migration routes through Mexico",
 
1275
  legend = list(type = "cats", pal = "infra_cats", filterable = "infra")),
1276
  list(kind = "layer", value = "patp-choro",
1277
  label = "Territorial analysis of poverty",
1278
+ tooltip = list(
1279
+ title = "PATP - territorial poverty",
1280
+ body = "CONEVAL territorial analysis: poverty share, extreme poverty, economic participation, occupation, remittances, road connectivity, predominant rural problem and crime focus. Pick a chip to switch the painted column.",
1281
+ meta = "Source: CONEVAL PATP 2020 + Poverty Census 2020"
1282
+ ),
1283
  legend = list(type = "ramp", pal = "patp", unit = "",
1284
  labels = "qualitative"),
1285
  # Variant legends shown when the matching categorical chip is active.
 
1350
  ),
1351
  charts = list(
1352
  list(id = "migrants_treemap_cause", label = "Cause of death",
1353
+ height = "40px"),
1354
  list(id = "migrants_treemap_demo", label = "Demographics (children / female / male)",
1355
+ height = "40px")
1356
  )),
1357
  list(title = "Missing persons and not located (RNPDNO)", kind = "stat",
1358
  stats = list(list(id = "n_desap", label = "Disappearances & no located", color = COL_RISK))),
 
1410
  setTimeout(initViewport, 800);
1411
  }, 400);
1412
 
1413
+ // Tooltip: positioned via JS to escape overflow:hidden containers.
1414
+ // Supports a rich form with header / body / meta when the trigger sets
1415
+ // data-tooltip-title and/or data-tooltip-meta in addition to data-tooltip.
1416
+ // Build the rich tooltip via DOM methods (textContent auto-escapes, no
1417
+ // quoting issues inside the surrounding R-quoted block).
1418
+ function _setRichTooltip(tip, title, body, meta) {
1419
+ while (tip.firstChild) tip.removeChild(tip.firstChild);
1420
+ function add(cls, text) {
1421
+ if (text == null || text === '') return;
1422
+ var d = document.createElement('div');
1423
+ d.className = cls;
1424
+ d.textContent = text;
1425
+ tip.appendChild(d);
1426
+ }
1427
+ add('depth-tooltip-title', title);
1428
+ add('depth-tooltip-body', body);
1429
+ add('depth-tooltip-meta', meta);
1430
+ }
1431
  document.addEventListener('mouseover', function(e) {
1432
  var el = e.target.closest('[data-tooltip]');
1433
  if (!el) { hideTooltip(); return; }
 
1438
  tip.className = 'depth-tooltip';
1439
  document.body.appendChild(tip);
1440
  }
1441
+ var title = el.dataset.tooltipTitle;
1442
+ var body = el.dataset.tooltip;
1443
+ var meta = el.dataset.tooltipMeta;
1444
+ if (title || meta) {
1445
+ tip.classList.add('is-rich');
1446
+ _setRichTooltip(tip, title, body, meta);
1447
+ } else {
1448
+ tip.classList.remove('is-rich');
1449
+ tip.textContent = body || '';
1450
+ }
1451
  tip.style.display = 'block';
1452
+ // Position to the right of the trigger by default; if there's not
1453
+ // enough room on the right (e.g. element in a left-pinned panel
1454
+ // whose right edge sits near the viewport edge), flip to the left.
1455
+ var r = el.getBoundingClientRect();
1456
+ var tw = tip.offsetWidth;
1457
+ var th = tip.offsetHeight;
1458
+ var vw = window.innerWidth;
1459
+ var pad = 8;
1460
+ var left = r.right + pad;
1461
+ if (left + tw > vw - pad) left = Math.max(pad, r.left - tw - pad);
1462
+ var top = r.top + r.height / 2;
1463
+ // Clamp vertically too so the (translateY -50%) tooltip stays onscreen.
1464
+ var half = th / 2;
1465
+ if (top - half < pad) top = pad + half;
1466
+ if (top + half > window.innerHeight - pad)
1467
+ top = window.innerHeight - pad - half;
1468
+ tip.style.left = left + 'px';
1469
+ tip.style.top = top + 'px';
1470
+ requestAnimationFrame(function() { tip.classList.add('is-visible'); });
1471
  });
1472
  document.addEventListener('mouseout', function(e) {
1473
  if (e.target.closest('[data-tooltip]')) hideTooltip();
 
1535
 
1536
  function hideTooltip() {
1537
  var tip = document.getElementById('depth-tooltip');
1538
+ if (!tip) return;
1539
+ tip.classList.remove('is-visible');
1540
+ tip.style.display = 'none';
1541
  }
1542
 
1543
  // -- Guided spotlight tour (Driver.js) --------------------------------------
 
2447
  ),
2448
  span(class = "acc-toggle-text", label)
2449
  ),
2450
+ if (!is.null(tooltip)) {
2451
+ # `tooltip` may be a plain string (legacy form) or a list with
2452
+ # title / body / meta (rich form). Rich-form keys are passed through
2453
+ # as data-* attributes; the JS reads them and renders the structured
2454
+ # layout (.depth-tooltip.is-rich).
2455
+ tip_body <- if (is.list(tooltip)) tooltip$body else tooltip
2456
+ tip_title <- if (is.list(tooltip)) tooltip$title else NULL
2457
+ tip_meta <- if (is.list(tooltip)) tooltip$meta else NULL
2458
  tags$span(class = "acc-info-wrap",
2459
+ `data-tooltip` = tip_body,
2460
+ `data-tooltip-title` = tip_title,
2461
+ `data-tooltip-meta` = tip_meta,
2462
+ role = "button",
2463
+ tabindex = "0",
2464
+ `aria-label` = paste0("Info: ",
2465
+ tip_title %||% substr(tip_body, 1, 60)),
2466
  HTML('<i data-lucide="info" style="width:12px;height:12px;stroke-width:2.5;color:var(--c-text-mid)"></i>')
2467
  )
2468
+ }
2469
  ),
2470
  if (!is.null(legend)) legend,
2471
  if (!is.null(legend_extras)) legend_extras
 
4704
  Female = "#7E4A86", # mauve
4705
  Male = "#3A4B5C") # slate
4706
 
4707
+ # Stacked-bar layout: one horizontal row of segments, widths proportional
4708
+ # to value. Returns the same shape as the treemap layout did (xmin/xmax/
4709
+ # ymin/ymax/value) so downstream code stays small.
4710
+ stacked_bar_layout <- function(df) {
4711
  df <- df[!is.na(df$value) & df$value > 0, , drop = FALSE]
4712
  if (nrow(df) == 0) return(NULL)
4713
  df <- df[order(-df$value), , drop = FALSE]
4714
  total <- sum(df$value)
4715
+ df$pct <- df$value / total
4716
+ df$xmax <- cumsum(df$pct)
4717
+ df$xmin <- c(0, head(df$xmax, -1))
4718
+ df$ymin <- 0
4719
+ df$ymax <- 1
4720
+ df
 
 
 
 
 
 
 
 
 
 
4721
  }
4722
 
4723
+ # Single-row stacked bar: each segment a category, width = its share. Labels
4724
+ # sit centred in the segment; percentages drop to a second line. Narrow
4725
+ # segments fall back to just the percentage. geom_text size is in mm:
4726
+ # 2.4mm ~ 6.8pt label, 2.0mm ~ 5.7pt percentage -- tuned for the ~60px tall
4727
+ # card slot the migrants card gives each chart.
4728
+ stacked_bar_plot <- function(df, pal) {
4729
+ laid <- stacked_bar_layout(df)
4730
  if (is.null(laid)) return(NULL)
4731
+ laid$pct_int <- round(100 * laid$pct)
4732
+ laid$pct_lbl <- paste0(laid$pct_int, "%")
4733
+ laid$cx <- (laid$xmin + laid$xmax) / 2
4734
+ laid$w <- laid$xmax - laid$xmin
4735
+ # Truncate the label to roughly what each segment can fit. Narrower than
4736
+ # ~6% of the bar (~16 px) gets just the percentage; below that, nothing.
4737
+ max_chars <- pmax(0L, as.integer(laid$w * 28))
4738
+ laid$short <- ifelse(nchar(laid$label) > max_chars,
4739
+ paste0(substr(laid$label, 1L, pmax(1L, max_chars - 1L)), "."),
4740
+ laid$label)
4741
+ laid$show_label <- laid$w >= 0.10 # ~ 33 px segments and up
4742
+ laid$show_pct <- laid$w >= 0.06
4743
+ label_df <- laid[laid$show_label, , drop = FALSE]
4744
+ pct_df <- laid[laid$show_pct, , drop = FALSE]
4745
+ pct_only <- !pct_df$show_label
4746
  ggplot(laid, aes(xmin = xmin, xmax = xmax,
4747
+ ymin = ymin, ymax = ymax, fill = label)) +
4748
+ # Soft 1px separator: white at low alpha reads on every palette colour
4749
+ # without dominating either segment (#ffffff99 = 60% opacity in R hex).
4750
+ geom_rect(color = "#ffffff99", linewidth = 0.4) +
4751
+ # Label centred high; percentage drops below it.
4752
+ { if (nrow(label_df) > 0)
4753
+ geom_text(data = label_df,
4754
+ aes(x = cx, y = 0.62, label = short),
4755
+ size = 2.4, color = "white", family = PLOT_FONT,
4756
+ fontface = "bold") } +
4757
+ { if (nrow(pct_df) > 0)
4758
+ geom_text(data = pct_df,
4759
+ aes(x = cx, y = ifelse(pct_only, 0.5, 0.32),
4760
+ label = pct_lbl),
4761
+ size = 2.0, color = "white", family = PLOT_FONT,
4762
+ alpha = 0.9) } +
4763
  scale_fill_manual(values = pal, na.value = "#334155") +
4764
  coord_cartesian(expand = FALSE, xlim = c(0, 1), ylim = c(0, 1)) +
4765
  theme_void() +
 
4799
  value = agg$n_dead,
4800
  color = unname(CAUSE_PAL[agg$cause_cat]),
4801
  stringsAsFactors = FALSE)
4802
+ stacked_bar_plot(summary_df, CAUSE_PAL)
4803
  }, bg = "transparent", res = 110)
4804
 
4805
  output$migrants_treemap_demo <- renderPlot({
 
4813
  color = unname(DEMO_PAL),
4814
  stringsAsFactors = FALSE
4815
  )
4816
+ stacked_bar_plot(summary_df, DEMO_PAL)
4817
  }, bg = "transparent", res = 110)
4818
 
4819
  # Shared helper: temporal bar chart by year for point event tables
styles.css CHANGED
@@ -878,11 +878,14 @@
878
  }
879
 
880
  .acc-toggle-text {
881
- font-size: 11.5px;
 
 
 
882
  color: var(--c-text-mid);
883
  font-family: var(--font-ui);
884
  transition: color var(--t-fast) var(--ease-out);
885
- line-height: 1.4;
886
  overflow: hidden;
887
  text-overflow: ellipsis;
888
  }
@@ -921,8 +924,13 @@
921
  transition: opacity var(--t-fast) var(--ease-out);
922
  }
923
 
924
- .acc-info-wrap:hover,
925
- .acc-info-wrap:focus-visible { opacity: 1; outline: none; }
 
 
 
 
 
926
 
927
  /* -- Layer legends ---------------------------------------------------------- */
928
  .acc-legend {
@@ -1201,25 +1209,72 @@
1201
  /* -- JS-positioned tooltip (inverted glass) -------------------------------- */
1202
  .depth-tooltip {
1203
  position: fixed;
1204
- background: oklch(15% 0.012 250 / 0.92);
1205
- backdrop-filter: blur(12px) saturate(140%);
1206
- -webkit-backdrop-filter: blur(12px) saturate(140%);
1207
  color: oklch(98% 0.003 250);
1208
- font-size: 11px;
1209
  font-family: var(--font-ui);
1210
  font-weight: 400;
1211
- line-height: 1.5;
1212
- padding: 8px 12px;
 
 
1213
  border-radius: var(--radius-md);
1214
- border: 1px solid oklch(100% 0 0 / 0.08);
1215
- max-width: 260px;
1216
  z-index: 99999;
1217
  pointer-events: none;
1218
  transform: translateY(-50%);
1219
  box-shadow: var(--depth-3);
1220
  word-break: break-word;
1221
  display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
  }
 
 
 
 
 
1223
 
1224
  /* -- Hide ALL MapLibre Draw native buttons -------------------------------- */
1225
  /* Custom Lucide-icon buttons in the nav-bar proxy-click these natives via */
@@ -1980,8 +2035,9 @@ button.mapbox-gl-draw_uncombine { display: none !important; }
1980
  border-radius: var(--radius-md) !important;
1981
  box-shadow: var(--depth-3) !important;
1982
  font-family: var(--font-ui) !important;
1983
- font-size: 11.5px !important;
1984
  line-height: 1.55 !important;
 
1985
  font-variant-numeric: tabular-nums;
1986
  padding: 10px 14px !important;
1987
  max-width: 280px;
 
878
  }
879
 
880
  .acc-toggle-text {
881
+ /* 12.5 px is the smallest size that stays comfortably legible at glass-card
882
+ contrast (c-text-mid on c-glass-1 is ~7.4:1, but the eye reads thinner
883
+ strokes slower than that ratio suggests). */
884
+ font-size: 12.5px;
885
  color: var(--c-text-mid);
886
  font-family: var(--font-ui);
887
  transition: color var(--t-fast) var(--ease-out);
888
+ line-height: 1.45;
889
  overflow: hidden;
890
  text-overflow: ellipsis;
891
  }
 
924
  transition: opacity var(--t-fast) var(--ease-out);
925
  }
926
 
927
+ .acc-info-wrap:hover { opacity: 1; }
928
+ .acc-info-wrap:focus-visible {
929
+ opacity: 1;
930
+ outline: 2px solid var(--c-focus);
931
+ outline-offset: 2px;
932
+ border-radius: var(--radius-sm);
933
+ }
934
 
935
  /* -- Layer legends ---------------------------------------------------------- */
936
  .acc-legend {
 
1209
  /* -- JS-positioned tooltip (inverted glass) -------------------------------- */
1210
  .depth-tooltip {
1211
  position: fixed;
1212
+ background: oklch(15% 0.012 250 / 0.94);
1213
+ backdrop-filter: blur(14px) saturate(150%);
1214
+ -webkit-backdrop-filter: blur(14px) saturate(150%);
1215
  color: oklch(98% 0.003 250);
1216
+ font-size: 12.5px;
1217
  font-family: var(--font-ui);
1218
  font-weight: 400;
1219
+ line-height: 1.55;
1220
+ letter-spacing: 0.005em;
1221
+ font-variant-numeric: tabular-nums;
1222
+ padding: 9px 13px;
1223
  border-radius: var(--radius-md);
1224
+ border: 1px solid oklch(100% 0 0 / 0.10);
1225
+ max-width: 280px;
1226
  z-index: 99999;
1227
  pointer-events: none;
1228
  transform: translateY(-50%);
1229
  box-shadow: var(--depth-3);
1230
  word-break: break-word;
1231
  display: none;
1232
+ /* Fade in via transition rather than springing the size; the JS pops the
1233
+ element by toggling display, then the next frame the opacity lifts. */
1234
+ opacity: 0;
1235
+ transition: opacity var(--t-fast) var(--ease-out);
1236
+ }
1237
+ .depth-tooltip.is-visible { opacity: 1; }
1238
+
1239
+ /* ---- Rich tooltip variant -------------------------------------------------
1240
+ When a trigger has data-tooltip-title and/or data-tooltip-meta in addition
1241
+ to data-tooltip, JS adds .is-rich and emits structured slots. The layout
1242
+ reads as a tiny card: tinted uppercase title, paragraph body, dim meta
1243
+ footer separated by a hairline divider. */
1244
+ .depth-tooltip.is-rich {
1245
+ padding: 0;
1246
+ max-width: 320px;
1247
+ }
1248
+ .depth-tooltip-title {
1249
+ padding: 9px 14px 6px;
1250
+ font-family: var(--font-ui);
1251
+ font-size: 10.5px;
1252
+ font-weight: 600;
1253
+ letter-spacing: 0.08em;
1254
+ text-transform: uppercase;
1255
+ color: oklch(82% 0.07 218); /* soft teal-on-dark; readable, low chroma */
1256
+ border-bottom: 1px solid oklch(100% 0 0 / 0.08);
1257
+ }
1258
+ .depth-tooltip-body {
1259
+ padding: 8px 14px;
1260
+ font-size: 12.5px;
1261
+ line-height: 1.6;
1262
+ color: oklch(95% 0.003 250);
1263
+ }
1264
+ .depth-tooltip-meta {
1265
+ padding: 6px 14px 9px;
1266
+ border-top: 1px solid oklch(100% 0 0 / 0.06);
1267
+ font-family: var(--font-ui);
1268
+ font-size: 10.5px;
1269
+ letter-spacing: 0.04em;
1270
+ color: oklch(70% 0 0);
1271
+ font-variant-numeric: tabular-nums;
1272
  }
1273
+ /* When only title+body or only body+meta are provided, suppress the empty
1274
+ border that would otherwise hang under the missing slot. */
1275
+ .depth-tooltip-title:last-child,
1276
+ .depth-tooltip-body:last-child { border-bottom: 0; padding-bottom: 9px; }
1277
+ .depth-tooltip-meta:first-child { border-top: 0; padding-top: 9px; }
1278
 
1279
  /* -- Hide ALL MapLibre Draw native buttons -------------------------------- */
1280
  /* Custom Lucide-icon buttons in the nav-bar proxy-click these natives via */
 
2035
  border-radius: var(--radius-md) !important;
2036
  box-shadow: var(--depth-3) !important;
2037
  font-family: var(--font-ui) !important;
2038
+ font-size: 12.5px !important;
2039
  line-height: 1.55 !important;
2040
+ letter-spacing: 0.005em;
2041
  font-variant-numeric: tabular-nums;
2042
  padding: 10px 14px !important;
2043
  max-width: 280px;