---
title:  "`r params$title`"
author: "`r params$author`"
date:   "`r format(Sys.Date(), '%d %B %Y')`"
output:
  html_document:
    theme:          null
    highlight:      null
    self_contained: true
    toc:            false
    mathjax:        null
params:
  result:      NULL
  title:       "Oyster Suitability Assessment"
  author:      ""
  species_key: "ostrea_edulis"
  top_n:       5
  validation:  NULL
  comparison:  NULL
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE,
                      fig.align = "center")

library(oystermapR)
library(plotly)
suppressPackageStartupMessages(library(leaflet))

result      <- params$result
species_key <- params$species_key
validation  <- params$validation
comparison  <- params$comparison

# ── Guard: ensure 'excluded' column exists and is logical ───────────────────
if (!"excluded" %in% names(result)) result$excluded <- FALSE
result$excluded <- as.logical(result$excluded)
result$excluded[is.na(result$excluded)] <- FALSE

tol       <- get_species_tolerances(species_key)
top_sites <- oystermapR:::.top_introduction_sites(result, n = params$top_n)

# ── Summary statistics ──────────────────────────────────────────────────────
n_total   <- nrow(result)
n_excl    <- sum(result$excluded, na.rm = TRUE)
n_scored  <- n_total - n_excl
n_high    <- sum(result$suitability_class == "High",     na.rm = TRUE)
n_mod     <- sum(result$suitability_class == "Moderate", na.rm = TRUE)
n_low     <- sum(result$suitability_class == "Low",      na.rm = TRUE)
n_vlow    <- sum(result$suitability_class == "Very Low", na.rm = TRUE)
mean_suit <- round(mean(result$suitability[!result$excluded], na.rm = TRUE), 3)
mean_comp <- if ("data_completeness" %in% names(result)) {
               round(mean(result$data_completeness[!result$excluded], na.rm = TRUE), 2)
             } else {
               NA
             }

# ── Colour palette ─────────────────────────────────────────────────────────
COL_TEAL   <- "#1a6b6b"
COL_SLATE  <- "#2c3e50"
COL_ORANGE <- "#d35400"
COL_HIGH   <- "#27ae60"
COL_MOD    <- "#e67e22"
COL_LOW    <- "#e74c3c"
COL_VLOW   <- "#c0392b"
COL_EXCL   <- "#95a5a6"

class_colours <- c(
  "High"     = COL_HIGH,
  "Moderate" = COL_MOD,
  "Low"      = COL_LOW,
  "Very Low" = COL_VLOW,
  "Excluded" = COL_EXCL
)

# Plotly base config (no mode bar clutter)
base_config <- list(displayModeBar = FALSE, responsive = TRUE)
base_layout <- list(
  font        = list(family = "Inter, -apple-system, sans-serif",
                     color  = COL_SLATE, size = 12),
  paper_bgcolor = "rgba(0,0,0,0)",
  plot_bgcolor  = "rgba(0,0,0,0)",
  margin        = list(t = 30, b = 40, l = 10, r = 10)
)
```

<style>
/* ── Reset & base ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --teal:       #1a6b6b;
  --teal-dark:  #145050;
  --teal-mid:   #2d9090;
  --teal-light: #e6f2f2;
  --slate:      #2c3e50;
  --orange:     #d35400;
  --bg:         #f5f7f8;
  --white:      #ffffff;
  --border:     #dde3e8;
  --text:       #2c3e50;
  --text-muted: #6c7a89;
  --shadow:     0 2px 10px rgba(0,0,0,0.08);
  --radius:     8px;
  --sidebar-w:  220px;
}

body {
  font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
}

/* ── Page header ─────────────────────────────────────────────────────────── */
.page-header {
  background: linear-gradient(135deg, var(--teal-dark) 0%, var(--teal) 60%, var(--teal-mid) 100%);
  color: white;
  padding: 36px 48px 32px;
}
.page-header h1 { font-size: 26px; font-weight: 700; margin-bottom: 6px; }
.page-header .meta {
  font-size: 13px; opacity: 0.8; display: flex; gap: 20px; flex-wrap: wrap;
  margin-top: 8px;
}
.page-header .meta span::before { content: "·"; margin-right: 8px; }
.page-header .meta span:first-child::before { content: ""; margin: 0; }
.page-header .badge {
  display: inline-block;
  background: rgba(255,255,255,0.15);
  border: 1px solid rgba(255,255,255,0.3);
  border-radius: 20px;
  padding: 2px 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-top: 10px;
}

/* ── Layout ──────────────────────────────────────────────────────────────── */
.page-body {
  display: flex;
  min-height: calc(100vh - 120px);
}

/* ── Sidebar navigation ──────────────────────────────────────────────────── */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--white);
  border-right: 1px solid var(--border);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  padding: 24px 0;
}
.sidebar-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 20px 8px;
}
.sidebar a {
  display: block;
  padding: 7px 20px;
  color: var(--text);
  text-decoration: none;
  font-size: 13px;
  border-left: 3px solid transparent;
  transition: all 0.15s;
}
.sidebar a:hover, .sidebar a.active {
  background: var(--teal-light);
  border-left-color: var(--teal);
  color: var(--teal);
}
.sidebar .nav-sub a {
  padding-left: 32px;
  font-size: 12px;
  color: var(--text-muted);
}

/* ── Main content ────────────────────────────────────────────────────────── */
.content {
  flex: 1;
  padding: 32px 40px 60px;
  max-width: 960px;
}

/* ── Section headings ────────────────────────────────────────────────────── */
.section { margin-bottom: 40px; }
.section-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--teal);
  border-bottom: 2px solid var(--teal-light);
  padding-bottom: 8px;
  margin-bottom: 18px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.section-title .icon { font-size: 16px; }
.subsection-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--slate);
  margin: 20px 0 10px;
}

/* ── Stat cards ──────────────────────────────────────────────────────────── */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
  margin-bottom: 24px;
}
.stat-card {
  background: var(--white);
  border-radius: var(--radius);
  padding: 16px 18px;
  box-shadow: var(--shadow);
  border-top: 3px solid var(--teal);
}
.stat-card.orange { border-top-color: var(--orange); }
.stat-card.high   { border-top-color: #27ae60; }
.stat-card.mod    { border-top-color: #e67e22; }
.stat-card.low    { border-top-color: #e74c3c; }
.stat-card.excl   { border-top-color: var(--text-muted); }
.stat-value { font-size: 28px; font-weight: 700; color: var(--slate); line-height: 1; }
.stat-label { font-size: 11px; color: var(--text-muted); margin-top: 4px;
              text-transform: uppercase; letter-spacing: 0.04em; }

/* ── Tables ──────────────────────────────────────────────────────────────── */
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.data-table th {
  background: var(--teal);
  color: white;
  text-align: left;
  padding: 9px 12px;
  font-weight: 600;
  font-size: 12px;
}
.data-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
.data-table tr:nth-child(even) td { background: #fafbfc; }
.data-table tr:hover td { background: var(--teal-light); }
.data-table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ── Class badges ────────────────────────────────────────────────────────── */
.class-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.class-High     { background: #d5f5e3; color: #1e8449; }
.class-Moderate { background: #fdebd0; color: #b7770d; }
.class-Low      { background: #fadbd8; color: #a93226; }
.class-Very-Low { background: #f2d7d5; color: #922b21; }
.class-Excluded { background: #f0f0f0; color: #666; }

/* ── Progress bar ────────────────────────────────────────────────────────── */
.progress-bar-wrap { background: #eee; border-radius: 4px; height: 8px; width: 120px; display: inline-block; vertical-align: middle; }
.progress-bar-fill { background: var(--teal); height: 100%; border-radius: 4px; }
.progress-bar-fill.warn { background: var(--orange); }
.progress-bar-fill.ok   { background: #27ae60; }

/* ── Alert / callout ─────────────────────────────────────────────────────── */
.callout {
  border-left: 4px solid var(--teal);
  background: var(--teal-light);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 12px 16px;
  margin: 14px 0;
  font-size: 13px;
}
.callout.warn  { border-color: var(--orange); background: #fef5ef; }
.callout.alert { border-color: #c0392b; background: #fdf0ef; }
.callout p { margin: 0; }
.callout strong { color: var(--teal); }
.callout.warn strong, .callout.warn { color: var(--orange); }

/* ── Plotly chart wrapper ────────────────────────────────────────────────── */
.chart-card {
  background: var(--white);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 18px 20px 10px;
  margin-bottom: 20px;
}
.chart-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 8px;
}

/* ── Metric row (validation) ─────────────────────────────────────────────── */
.metric-row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.metric-pill {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 6px 16px;
  font-size: 13px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.metric-pill strong { color: var(--teal); }

/* ── Footer ──────────────────────────────────────────────────────────────── */
.page-footer {
  background: var(--slate);
  color: rgba(255,255,255,0.6);
  font-size: 11px;
  padding: 16px 48px;
  display: flex;
  justify-content: space-between;
}
.page-footer a { color: rgba(255,255,255,0.8); }

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
  .sidebar { display: none; }
  .content { padding: 20px; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>

<!-- ── Page header ──────────────────────────────────────────────────────── -->
```{r page-header, results='asis', echo=FALSE}
cat(paste0(
  '<div class="page-header">',
  '<h1>', params$title, '</h1>',
  '<div class="meta">',
  '<span><em>', tol$latin_name, '</em> &mdash; ', tol$common_name, '</span>',
  if (nchar(params$author) > 0) paste0('<span>', params$author, '</span>') else '',
  '<span>', format(Sys.Date(), '%d %B %Y'), '</span>',
  '</div>',
  '<div>',
  '<span class="badge">oystermapR ', as.character(packageVersion("oystermapR")), '</span>',
  '&nbsp;',
  '<span class="badge">', tol$data_quality, ' data quality</span>',
  if (!is.null(validation)) '<span class="badge">Validated</span>' else '',
  '</div>',
  '</div>'
))
```

<div class="page-body">

<!-- ── Sidebar ─────────────────────────────────────────────────────────── -->
```{r sidebar, results='asis', echo=FALSE}
cat(paste0(
  '<nav class="sidebar">',
  '<div class="sidebar-label">Contents</div>',
  '<a href="#summary">Executive Summary</a>',
  '<a href="#map">Spatial Map</a>',
  '<a href="#species">Species Profile</a>',
  '<a href="#results">Suitability Results</a>',
  '<div class="nav-sub">',
  '<a href="#distribution">Score Distribution</a>',
  '<a href="#classes">Class Breakdown</a>',
  '</div>',
  '<a href="#variables">Variable Scoring</a>',
  '<a href="#completeness">Data Completeness</a>',
  '<a href="#sites">Top Introduction Sites</a>',
  if (!is.null(comparison)) '<a href="#competition">Competition Adjustment</a>' else '',
  if (!is.null(validation)) '<a href="#validation">Validation</a>' else '',
  '<a href="#methodology">Methodology</a>',
  '</nav>'
))
```

<!-- ── Main content ────────────────────────────────────────────────────── -->
<div class="content">

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 1. Executive Summary                                                   -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="summary" class="section">
<div class="section-title"><span class="icon">📊</span> Executive Summary</div>

<div class="stat-grid">
<div class="stat-card">
<div class="stat-value">`r n_total`</div>
<div class="stat-label">Survey locations</div>
</div>
<div class="stat-card excl">
<div class="stat-value">`r n_excl`</div>
<div class="stat-label">Excluded</div>
</div>
<div class="stat-card orange">
<div class="stat-value">`r mean_suit`</div>
<div class="stat-label">Mean suitability</div>
</div>
<div class="stat-card high">
<div class="stat-value">`r n_high`</div>
<div class="stat-label">High scoring</div>
</div>
`r if (!is.na(mean_comp)) knitr::asis_output(paste0('<div class="stat-card"><div class="stat-value">', scales::percent(mean_comp, accuracy=1), '</div><div class="stat-label">Mean data completeness</div></div>'))`
</div>

```{r exec-table}
df_exec <- data.frame(
  Metric = c("Total survey locations","Excluded (hard limits)","Scored locations",
             "Mean suitability (scored)","High (\u2265 0.70)","Moderate (0.45\u20130.70)",
             "Low (0.20\u20130.45)","Very Low (< 0.20)"),
  Value  = c(n_total, n_excl, n_scored, mean_suit, n_high, n_mod, n_low, n_vlow)
)
knitr::kable(df_exec, col.names = c("Metric","Value"),
             align = c("l","r"),
             table.attr = 'class="data-table"')
```

</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 2. Spatial Map                                                         -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="map" class="section">
<div class="section-title"><span class="icon">🗺️</span> Spatial Map</div>
<p style="color:var(--text-muted);margin-bottom:14px;">Survey locations coloured by suitability class. Click any marker for coordinates and score. Zoom and pan to explore the survey area.</p>

```{r leaflet-map}
has_coords <- all(c("lat","lon") %in% names(result)) &&
              sum(!is.na(result$lat) & !is.na(result$lon)) > 0

if (has_coords) {
  map_df <- result[!is.na(result$lat) & !is.na(result$lon), ]

  pal <- leaflet::colorFactor(
    palette = c("#27ae60","#e67e22","#e74c3c","#c0392b","#95a5a6"),
    levels  = c("High","Moderate","Low","Very Low","Excluded"),
    na.color = "#95a5a6"
  )

  # Uncertainty ring radius: use ci_width if available, else fixed 80 m
  has_ci <- all(c("suit_ci_lower","suit_ci_upper") %in% names(map_df))

  popup_html <- paste0(
    "<b>Suitability:</b> ", round(map_df$suitability, 3), "<br>",
    "<b>Class:</b> ", as.character(map_df$suitability_class), "<br>",
    if (has_ci) paste0("<b>90% CI:</b> [",
                       round(map_df$suit_ci_lower, 3), ", ",
                       round(map_df$suit_ci_upper, 3), "]<br>") else "",
    "<b>Completeness:</b> ",
    if ("data_completeness" %in% names(map_df))
      paste0(round(map_df$data_completeness * 100), "%")
    else "n/a", "<br>",
    "<b>Lat:</b> ", round(map_df$lat, 5), " &nbsp; <b>Lon:</b> ", round(map_df$lon, 5)
  )

  m <- leaflet::leaflet(map_df, options = leaflet::leafletOptions(zoomSnap = 0.5)) |>
    leaflet::addProviderTiles(
      "CartoDB.Positron",
      options = leaflet::providerTileOptions(opacity = 0.85)
    ) |>
    leaflet::addCircleMarkers(
      lng    = ~lon, lat = ~lat,
      radius = 6,
      color  = ~pal(suitability_class),
      fillColor  = ~pal(suitability_class),
      fillOpacity = 0.85,
      stroke = TRUE, weight = 1,
      opacity = 0.9,
      popup  = popup_html
    ) |>
    leaflet::addLegend(
      position = "bottomright",
      pal      = pal,
      values   = ~suitability_class,
      title    = "Suitability",
      opacity  = 0.9
    )

  # If uncertainty CI available, add a faint ring to show spread
  if (has_ci) {
    ci_width <- map_df$suit_ci_upper - map_df$suit_ci_lower
    ring_r   <- 4 + round(ci_width * 20)   # scale ring with CI width
    m <- m |> leaflet::addCircleMarkers(
      lng    = ~lon, lat = ~lat,
      radius = ring_r,
      color  = ~pal(suitability_class),
      fillOpacity = 0,
      stroke = TRUE, weight = 1, opacity = 0.25
    )
  }

  m
} else {
  cat('<div class="callout warn"><p>No valid coordinates found in result dataframe — map unavailable.</p></div>')
}
```

</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 3. Species Profile                                                     -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="species" class="section">
<div class="section-title"><span class="icon">🦪</span> Species Profile</div>

```{r species-table}
sp_df <- data.frame(
  Parameter = c("Latin name","Common name","Region","Data quality"),
  Value     = c(paste0("*", tol$latin_name, "*"), tol$common_name,
                tol$region, tol$data_quality),
  stringsAsFactors = FALSE
)
knitr::kable(sp_df, col.names = c("Parameter","Value"),
             table.attr = 'class="data-table"')
```

<div class="subsection-title">Hard Exclusion Thresholds</div>

```{r exclusion-table}
excl_rows <- lapply(names(tol$exclusions), function(nm) {
  e <- tol$exclusions[[nm]]
  data.frame(
    Variable = nm,
    Min      = if (!is.null(e$min)) {
                 as.character(e$min)
               } else if (!is.null(e$min_cold)) {
                 paste0(e$min_cold, " (cold)")
               } else {
                 "\u2014"
               },
    Max      = if (!is.null(e$max))      as.character(e$max)       else "\u2014",
    Unit     = if (!is.null(e$unit))     e$unit                    else "\u2014",
    Note     = if (!is.null(e$note))     e$note                    else "",
    stringsAsFactors = FALSE
  )
})
knitr::kable(do.call(rbind, excl_rows),
             table.attr = 'class="data-table"',
             align = c("l","r","r","l","l"))
```

</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 3. Suitability Results                                                 -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="results" class="section">
<div class="section-title"><span class="icon">🗺️</span> Suitability Results</div>

<div id="distribution">
<div class="subsection-title">Score Distribution</div>
<div class="chart-card">
<div class="chart-title">Suitability score frequency — all scored locations</div>

```{r score-histogram, fig.height=3}
scored_df <- result[!result$excluded & !is.na(result$suitability), ]

# Colour each bar by its class
breaks   <- seq(0, 1, by = 0.05)
bin_mid  <- (breaks[-length(breaks)] + breaks[-1]) / 2
bin_col  <- dplyr::case_when(
  bin_mid >= 0.70 ~ COL_HIGH,
  bin_mid >= 0.45 ~ COL_MOD,
  bin_mid >= 0.20 ~ COL_LOW,
  TRUE            ~ COL_VLOW
)

plot_ly(x = ~scored_df$suitability,
        type = "histogram",
        xbins = list(start = 0, end = 1, size = 0.05),
        marker = list(color = bin_col[
          findInterval(
            seq(0.025, 0.975, 0.05),
            c(0.20, 0.45, 0.70), rightmost.closed = TRUE) + 1
        ],
        line = list(color = "white", width = 0.5))) %>%
  layout(
    xaxis  = list(title = "Suitability Score", range = c(0,1),
                  tickformat = ".2f"),
    yaxis  = list(title = "Locations"),
    shapes = list(list(type="line", x0=mean_suit, x1=mean_suit,
                       y0=0, y1=1, yref="paper",
                       line=list(color=COL_SLATE, dash="dot", width=1.5))),
    annotations = list(list(x=mean_suit, y=1, yref="paper",
                            text=paste("Mean:", mean_suit),
                            showarrow=FALSE, xanchor="left",
                            font=list(size=11, color=COL_SLATE))),
    legend = list(orientation="h"),
    font   = base_layout$font,
    paper_bgcolor = base_layout$paper_bgcolor,
    plot_bgcolor  = base_layout$plot_bgcolor,
    margin = list(t=20, b=40, l=10, r=10)
  ) %>%
  config(displayModeBar = FALSE, responsive = TRUE)
```

</div>
</div>

<div id="classes">
<div class="subsection-title">Class Breakdown</div>
<div class="chart-card">
<div class="chart-title">Locations per suitability class</div>

```{r class-breakdown, fig.height=2.2}
classes  <- c("High","Moderate","Low","Very Low","Excluded")
counts   <- c(n_high, n_mod, n_low, n_vlow, n_excl)
pct      <- round(counts / n_total * 100, 1)

plot_ly(
  x     = counts,
  y     = factor(classes, levels = rev(classes)),
  type  = "bar",
  orientation = "h",
  marker = list(color = rev(unname(class_colours))),
  text   = paste0(counts, " (", pct, "%)"),
  textposition = "outside",
  hovertemplate = "%{y}: %{x} locations<extra></extra>"
) %>%
  layout(
    xaxis  = list(title = "Locations", showgrid = TRUE),
    yaxis  = list(title = "", tickfont = list(size = 12)),
    font   = base_layout$font,
    paper_bgcolor = base_layout$paper_bgcolor,
    plot_bgcolor  = base_layout$plot_bgcolor,
    margin = list(t=10, b=40, l=10, r=60)
  ) %>%
  config(displayModeBar = FALSE, responsive = TRUE)
```

</div>
</div>
</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 4. Variable Scoring                                                    -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="variables" class="section">
<div class="section-title"><span class="icon">📐</span> Variable Scoring</div>

<div class="chart-card">
<div class="chart-title">Mean score per environmental variable (non-excluded locations)</div>

```{r variable-scores, fig.height=4}
score_cols <- grep("^score_", names(result), value = TRUE)
if (length(score_cols) > 0) {
  non_excl  <- result[!result$excluded, ]
  var_names <- gsub("^score_", "", score_cols)
  var_means <- sapply(score_cols, function(cn)
                 round(mean(non_excl[[cn]], na.rm = TRUE), 3))
  var_means <- sort(var_means)
  bar_cols  <- dplyr::case_when(
    var_means >= 0.70 ~ COL_HIGH,
    var_means >= 0.45 ~ COL_MOD,
    TRUE              ~ COL_LOW
  )

  plot_ly(
    x    = var_means,
    y    = factor(names(var_means), levels = names(var_means)),
    type = "bar",
    orientation = "h",
    marker = list(color = bar_cols),
    text   = sprintf("%.3f", var_means),
    textposition = "outside",
    hovertemplate = "%{y}: %{x:.3f}<extra></extra>"
  ) %>%
    layout(
      xaxis  = list(title = "Mean score", range = c(0, 1.12),
                    tickformat = ".2f",
                    shapes = list(
                      list(type="line", x0=0.45, x1=0.45,
                           y0=0, y1=1, yref="paper",
                           line=list(color="#ccc", dash="dot", width=1)),
                      list(type="line", x0=0.70, x1=0.70,
                           y0=0, y1=1, yref="paper",
                           line=list(color="#bbb", dash="dot", width=1))
                    )),
      yaxis  = list(title = "", tickfont = list(size = 12)),
      font   = base_layout$font,
      paper_bgcolor = base_layout$paper_bgcolor,
      plot_bgcolor  = base_layout$plot_bgcolor,
      margin = list(t=10, b=40, l=10, r=70)
    ) %>%
    config(displayModeBar = FALSE, responsive = TRUE)
}
```

</div>

<div class="subsection-title">Most Common Limiting Factors</div>

```{r limiting-factors}
if ("limiting_factors" %in% names(result)) {
  lf_all <- unlist(strsplit(
    result$limiting_factors[!is.na(result$limiting_factors)], ", "))
  if (length(lf_all) > 0) {
    lf_tbl <- sort(table(lf_all), decreasing = TRUE)
    lf_df  <- data.frame(
      Variable        = names(lf_tbl),
      `Times limiting` = as.integer(lf_tbl),
      `% of scored`    = paste0(round(as.integer(lf_tbl) / n_scored * 100, 1), "%"),
      stringsAsFactors = FALSE,
      check.names = FALSE
    )
    knitr::kable(lf_df, row.names = FALSE,
                 table.attr = 'class="data-table"',
                 align = c("l","r","r"))
  }
}
```

</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 5. Data Completeness                                                   -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="completeness" class="section">
<div class="section-title"><span class="icon">📋</span> Data Completeness</div>

```{r completeness-section}
has_completeness <- "data_completeness" %in% names(result)
```

`r if (!has_completeness) knitr::asis_output('<div class="callout warn"><p><strong>No completeness data.</strong> Run <code>score_locations()</code> with the latest oystermapR to populate the <code>data_completeness</code> column.</p></div>')`

```{r completeness-chart, eval=has_completeness, fig.height=2.5}
non_excl <- result[!result$excluded & !is.na(result$data_completeness), ]
comp_vals <- non_excl$data_completeness

# Distribution of completeness values
comp_breaks <- seq(0, 1, by = 0.1)
comp_pct    <- round(comp_vals * 100)

plot_ly(
  x    = ~comp_vals,
  type = "histogram",
  xbins = list(start = 0, end = 1, size = 0.1),
  marker = list(
    color = dplyr::case_when(
      seq(0.05, 0.95, 0.1) >= 0.8 ~ COL_HIGH,
      seq(0.05, 0.95, 0.1) >= 0.5 ~ COL_MOD,
      TRUE                         ~ COL_LOW
    ),
    line = list(color = "white", width = 0.5)
  )
) %>%
  layout(
    title  = list(text = "Distribution of data completeness scores",
                  font = list(size = 12, color = "#6c7a89"), x = 0),
    xaxis  = list(title = "Data completeness (fraction of variables scored)",
                  range = c(0, 1), tickformat = ".0%"),
    yaxis  = list(title = "Locations"),
    font   = base_layout$font,
    paper_bgcolor = base_layout$paper_bgcolor,
    plot_bgcolor  = base_layout$plot_bgcolor,
    margin = list(t=30, b=40, l=10, r=10)
  ) %>%
  config(displayModeBar = FALSE, responsive = TRUE)
```

```{r completeness-stats, eval=has_completeness}
non_excl <- result[!result$excluded & !is.na(result$data_completeness), ]
comp_vals <- non_excl$data_completeness

comp_df <- data.frame(
  Metric = c("Mean completeness","Median completeness",
             "Fully scored (100%)","> 80% scored",
             "< 50% scored (low confidence)"),
  Value  = c(
    scales::percent(mean(comp_vals), accuracy = 0.1),
    scales::percent(median(comp_vals), accuracy = 0.1),
    paste0(sum(comp_vals >= 1.0), " locations (", round(mean(comp_vals >= 1.0)*100,1), "%)"),
    paste0(sum(comp_vals >= 0.8), " locations (", round(mean(comp_vals >= 0.8)*100,1), "%)"),
    paste0(sum(comp_vals <  0.5), " locations (", round(mean(comp_vals <  0.5)*100,1), "%)")
  ),
  stringsAsFactors = FALSE
)

knitr::kable(comp_df, col.names = c("Metric","Value"),
             table.attr = 'class="data-table"', align = c("l","r"))
```

`r if (has_completeness && mean(result$data_completeness[!result$excluded], na.rm=TRUE) < 0.5) knitr::asis_output('<div class="callout warn"><p><strong>Low average data completeness.</strong> Fewer than half of the tolerance-spec variables had data for a typical location. Suitability scores will under-weight missing variables. Consider adding more sensor types (CTD, turbidity, substrate survey) before using results for site selection.</p></div>')`

</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 6. Top Introduction Sites                                              -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="sites" class="section">
<div class="section-title"><span class="icon">📍</span> Top Introduction Sites</div>

<div class="callout warn">
<p><strong>Regulatory notice:</strong> Confirm seabed lease status, biosecurity requirements, and species consent with the relevant authority before any stocking activity.</p>
</div>

```{r top-sites}
if (!is.null(top_sites) && nrow(top_sites) > 0) {
  display_cols <- intersect(
    c("lat","lon","suitability","suitability_class","patch_radius_m",
      "depth","temperature","salinity","current_velocity"),
    names(top_sites)
  )
  ts <- top_sites[, display_cols, drop = FALSE]
  ts$lat  <- round(ts$lat,  5)
  ts$lon  <- round(ts$lon,  5)
  ts$suitability <- round(ts$suitability, 3)

  # Format class as badge HTML
  ts$suitability_class <- paste0(
    '<span class="class-badge class-',
    gsub(" ", "-", ts$suitability_class), '">',
    ts$suitability_class, '</span>'
  )

  knitr::kable(ts, row.names = TRUE, escape = FALSE,
               table.attr = 'class="data-table"')
} else {
  cat('<div class="callout warn"><p>No sites above the suitability threshold were found.</p></div>')
}
```

</div>

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 7. Competition Adjustment (optional)                                   -->
<!-- ══════════════════════════════════════════════════════════════════════ -->

```{r competition-section, results='asis'}
if (!is.null(comparison)) {
  cat('<div id="competition" class="section">\n')
  cat('<div class="section-title"><span class="icon">⚔️</span> Competition Adjustment</div>\n')
  cat('<p style="color:var(--text-muted);margin-bottom:14px;">In NW Europe, established <em>Magallana gigas</em> beds outcompete <em>Ostrea edulis</em> for settling space. Scores below reflect the post-hoc competitive pressure penalty applied in <code>compare_species()</code>.</p>\n')
}
```

```{r competition-chart, eval=!is.null(comparison), fig.height=3}
if (!is.null(comparison)) {
  adj_col  <- "suit_ostrea_edulis_competition_adj"
  raw_col  <- "suit_ostrea_edulis"
  gigas_col <- "suit_magallana_gigas"

  if (adj_col %in% names(comparison) && raw_col %in% names(comparison)) {
    raw  <- comparison[[raw_col]]
    adj  <- comparison[[adj_col]]
    diff <- raw - adj

    plot_ly() %>%
      add_trace(x = raw, type = "histogram",
                name = "Raw O. edulis",
                marker = list(color = COL_TEAL, opacity = 0.65),
                xbins = list(start=0, end=1, size=0.05)) %>%
      add_trace(x = adj, type = "histogram",
                name = "Competition-adjusted",
                marker = list(color = COL_ORANGE, opacity = 0.65),
                xbins = list(start=0, end=1, size=0.05)) %>%
      layout(
        barmode = "overlay",
        xaxis   = list(title = "Suitability Score", range = c(0,1)),
        yaxis   = list(title = "Locations"),
        legend  = list(orientation = "h", y = 1.12),
        font    = base_layout$font,
        paper_bgcolor = base_layout$paper_bgcolor,
        plot_bgcolor  = base_layout$plot_bgcolor,
        margin  = list(t=40, b=40, l=10, r=10)
      ) %>%
      config(displayModeBar = FALSE, responsive = TRUE)
  }
}
```

```{r competition-stats, eval=!is.null(comparison), results='asis'}
if (!is.null(comparison)) {
  adj_col <- "suit_ostrea_edulis_competition_adj"
  raw_col <- "suit_ostrea_edulis"
  if (adj_col %in% names(comparison) && raw_col %in% names(comparison)) {
    n_adj      <- sum(comparison[[raw_col]] != comparison[[adj_col]], na.rm=TRUE)
    mean_delta <- round(mean(comparison[[raw_col]] - comparison[[adj_col]], na.rm=TRUE), 3)
    cat(paste0('<div class="callout"><p><strong>', n_adj, '</strong> locations were downscored. ',
               'Mean suitability reduction: <strong>', mean_delta, '</strong> points.</p></div>\n'))
  }
  cat('</div>\n')
}
```

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 8. Validation Results (optional)                                       -->
<!-- ══════════════════════════════════════════════════════════════════════ -->

```{r validation-section, results='asis'}
if (!is.null(validation)) {
  cat('<div id="validation" class="section">\n')
  cat('<div class="section-title"><span class="icon">✅</span> Model Validation</div>\n')

  auc  <- validation$auc
  tss  <- validation$tss
  sens <- validation$sensitivity
  spec <- validation$specificity
  f1   <- validation$f1
  brier <- validation$brier_score
  n_p  <- validation$n_presences
  n_a  <- validation$n_absences

  cat(paste0(
    '<div class="metric-row">',
    '<div class="metric-pill"><strong>AUC</strong> ', auc, '</div>',
    '<div class="metric-pill"><strong>TSS</strong> ', tss, '</div>',
    '<div class="metric-pill"><strong>Sensitivity</strong> ', sens, '</div>',
    '<div class="metric-pill"><strong>Specificity</strong> ', spec, '</div>',
    '<div class="metric-pill"><strong>F1</strong> ', f1, '</div>',
    '<div class="metric-pill"><strong>Brier</strong> ', brier, '</div>',
    '</div>\n'
  ))

  interp_class <- if (auc >= 0.8) "callout" else if (auc >= 0.6) "callout warn" else "callout alert"
  interp_text  <- if (auc >= 0.8) {
    paste0("Good discrimination (AUC = ", auc, "). The model reliably distinguishes suitable from unsuitable habitat.")
  } else if (auc >= 0.6) {
    paste0("Moderate discrimination (AUC = ", auc, "). The model has useful but imperfect predictive skill. Consider adding more environmental variables.")
  } else {
    paste0("Poor discrimination (AUC = ", auc, "). Model performance is near-random. Review variable coverage and AHP weights.")
  }

  cat(paste0('<div class="', interp_class, '"><p>', interp_text, '</p></div>\n'))
}
```

```{r validation-roc, eval=!is.null(validation), fig.height=3.5}
if (!is.null(validation) && !is.null(validation$roc_df)) {
  roc <- validation$roc_df
  fpr <- 1 - roc$specificity
  tpr <- roc$sensitivity

  plot_ly() %>%
    add_trace(x = c(0,1), y = c(0,1), type = "scatter", mode = "lines",
              line = list(color = "#cccccc", dash = "dot"),
              name = "Random (AUC = 0.5)",
              hoverinfo = "none") %>%
    add_trace(x = fpr, y = tpr, type = "scatter", mode = "lines",
              line = list(color = COL_TEAL, width = 2.5),
              fill = "tozeroy",
              fillcolor = paste0(gsub("#","rgba(", COL_TEAL), ",0.1)"),
              name = paste0("Model (AUC = ", validation$auc, ")"),
              hovertemplate = "FPR: %{x:.2f}<br>TPR: %{y:.2f}<extra></extra>") %>%
    add_trace(x = 1 - validation$specificity,
              y = validation$sensitivity,
              type = "scatter", mode = "markers",
              marker = list(color = COL_ORANGE, size = 10, symbol = "diamond"),
              name = paste0("Optimal threshold (", validation$optimal_threshold, ")"),
              hovertemplate = paste0("Threshold: ", validation$optimal_threshold,
                                     "<br>Sensitivity: ", validation$sensitivity,
                                     "<br>Specificity: ", validation$specificity,
                                     "<extra></extra>")) %>%
    layout(
      xaxis  = list(title = "1 \u2212 Specificity (False Positive Rate)",
                    range = c(0,1), tickformat = ".1f"),
      yaxis  = list(title = "Sensitivity (True Positive Rate)",
                    range = c(0,1), tickformat = ".1f"),
      legend = list(orientation = "h", y = -0.2),
      font   = base_layout$font,
      paper_bgcolor = base_layout$paper_bgcolor,
      plot_bgcolor  = base_layout$plot_bgcolor,
      margin = list(t=20, b=60, l=10, r=10)
    ) %>%
    config(displayModeBar = FALSE, responsive = TRUE)
}
```

```{r validation-table, eval=!is.null(validation), results='asis'}
if (!is.null(validation)) {
  val_df <- data.frame(
    Metric = c("AUC","True Skill Statistic (TSS)","Sensitivity",
               "Specificity","F1 score","Brier score",
               "Optimal threshold","Presences in records","Absences in records",
               "Unmatched records"),
    Value  = c(validation$auc, validation$tss, validation$sensitivity,
               validation$specificity, validation$f1, validation$brier_score,
               validation$optimal_threshold, validation$n_presences,
               validation$n_absences, validation$n_unmatched),
    Interpretation = c(
      "0.5 = random, 1.0 = perfect", "> 0.4 = useful skill",
      "True positive rate at optimal threshold",
      "True negative rate at optimal threshold",
      "Harmonic mean of precision and sensitivity",
      "0 = perfect, 0.25 = uninformative (lower = better)",
      "Youden's J statistic maximiser","","","Dropped (no nearby prediction)"
    ),
    stringsAsFactors = FALSE
  )
  knitr::kable(val_df, table.attr = 'class="data-table"',
               align = c("l","r","l"))
  cat('</div>\n')
}
```

<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- 9. Methodology                                                         -->
<!-- ══════════════════════════════════════════════════════════════════════ -->
<div id="methodology" class="section">
<div class="section-title"><span class="icon">⚙️</span> Methodology</div>

Suitability scores were generated using the `oystermapR` R package. Environmental variables were scored against species-specific tolerance parameters and combined using a **rank-weighted mean** (rank 1 = highest ecological importance). Missing variables were excluded from the weighted average with their weight redistributed proportionally to present variables. The `data_completeness` column records the fraction of tolerance-spec variables that contributed to each location's score.

**Exclusion criteria** were applied as hard stops before scoring. Locations failing temperature, salinity, or dissolved oxygen limits were excluded entirely (suitability = 0).

**Seasonal variation** was applied to temperature scoring where survey dates were present. Optimal temperature ranges shift between seasons following published spawning and growth thresholds.

**Spatial interpolation** of the suitability surface used Inverse Distance Weighting (IDW, power = 2) with an automatically calculated search radius.

**Primary tolerance sources:**

| Species | Source |
|---------|--------|
| *Ostrea edulis* | Pogoda *et al.* (2023) *Aquatic Conservation* 33(9):940–960 |
| *Magallana gigas* | FAO Cultured Aquatic Species Fact Sheets; Bayne *et al.* (2017) *Ann. Rev. Marine Sci.* |
| *Crassostrea angulata* | Huvet *et al.* (2018) *Aquatic Living Resources* 31:23 |
| *Ostrea stentina* | Sendra *et al.* (2022) *Regional Studies in Marine Science* 53:102434 |

</div>

</div><!-- end .content -->
</div><!-- end .page-body -->

<!-- ── Footer ─────────────────────────────────────────────────────────── -->
<div class="page-footer">
  <span>Generated by oystermapR `r packageVersion("oystermapR")` &bull; `r format(Sys.Date(), "%d %B %Y")`</span>
  <span>`r tol$latin_name` &bull; `r tol$common_name`</span>
</div>

<script>
// Highlight active sidebar link based on scroll position
(function() {
  var links = document.querySelectorAll('.sidebar a');
  var sections = [];
  links.forEach(function(l) {
    var id = l.getAttribute('href').replace('#','');
    var el = document.getElementById(id);
    if (el) sections.push({ el: el, link: l });
  });
  function onScroll() {
    var pos = window.scrollY + 80;
    var active = null;
    sections.forEach(function(s) {
      if (s.el.offsetTop <= pos) active = s;
    });
    links.forEach(function(l) { l.classList.remove('active'); });
    if (active) active.link.classList.add('active');
  }
  window.addEventListener('scroll', onScroll, { passive: true });
  onScroll();
})();
</script>
