// app.jsx — main shell: token gate, station picker, theme, tabs, polling, embedded styling.

const { useState, useEffect, useMemo, useCallback } = React;

const APP_CSS = `
  :root {
    --bg: #0b0d10;
    --bg-elev: #11151a;
    --bg-elev-2: #161b21;
    --line: rgba(255,255,255,0.07);
    --line-strong: rgba(255,255,255,0.13);
    --fg: #e8eaed;
    --fg-dim: rgba(255,255,255,0.62);
    --fg-faint: rgba(255,255,255,0.42);
    --accent: oklch(0.78 0.14 75);
    --cool: oklch(0.78 0.10 200);
    --ok: oklch(0.74 0.14 145);
    --warn: oklch(0.74 0.16 60);
    --bg-gradient:
      radial-gradient(circle at 30% 0%, oklch(0.22 0.018 240) 0%, transparent 50%),
      radial-gradient(circle at 80% 100%, oklch(0.20 0.020 220) 0%, transparent 50%),
      linear-gradient(180deg, #0a0c0f 0%, #0b0d10 100%);
    --shadow-root: 0 0 40px rgba(0,0,0,0.5);
    --shadow-nav: 0 8px 24px rgba(0,0,0,0.4);
    --shadow-toast: 0 8px 24px rgba(0,0,0,0.35);
    --attribution-bg: rgba(11,13,16,0.7);
    --save-btn-fg: #1a1306;
    --switch-knob: #fff;
  }
  :root[data-theme="light"] {
    --bg: #f4f4f0;
    --bg-elev: #ffffff;
    --bg-elev-2: #ebeae6;
    --line: rgba(0,0,0,0.08);
    --line-strong: rgba(0,0,0,0.16);
    --fg: #1a1d21;
    --fg-dim: rgba(0,0,0,0.62);
    --fg-faint: rgba(0,0,0,0.44);
    --accent: oklch(0.60 0.15 60);
    --cool: oklch(0.55 0.12 220);
    --ok: oklch(0.55 0.14 145);
    --warn: oklch(0.62 0.17 50);
    --bg-gradient:
      radial-gradient(circle at 30% 0%, oklch(0.96 0.02 75) 0%, transparent 55%),
      radial-gradient(circle at 80% 100%, oklch(0.94 0.025 220) 0%, transparent 55%),
      linear-gradient(180deg, #f6f5f1 0%, #efeeea 100%);
    --shadow-root: 0 0 40px rgba(0,0,0,0.08);
    --shadow-nav: 0 8px 24px rgba(0,0,0,0.10);
    --shadow-toast: 0 8px 24px rgba(0,0,0,0.10);
    --attribution-bg: rgba(255,255,255,0.85);
    --save-btn-fg: #ffffff;
    --switch-knob: #ffffff;
  }
  html, body { margin: 0; padding: 0; }
  body {
    min-height: 100vh;
    background: var(--bg);
    background-image: var(--bg-gradient);
    background-attachment: fixed;
    color: var(--fg);
    font-family: 'Inter', -apple-system, system-ui, sans-serif;
    -webkit-font-smoothing: antialiased;
  }

  #root {
    max-width: 480px;
    margin: 0 auto;
    min-height: 100vh;
    background: var(--bg);
    background-image: var(--bg-gradient);
    display: flex;
    flex-direction: column;
    position: relative;
    box-shadow: var(--shadow-root);
  }

  .app { display: flex; flex-direction: column; min-height: 100vh; }
  .mono { font-family: 'JetBrains Mono', monospace; font-feature-settings: 'tnum'; }

  /* ── Header ───────────────────────────────────────────── */
  .app-header {
    padding: max(env(safe-area-inset-top), 14px) 16px 10px;
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px;
    border-bottom: 1px solid var(--line);
    background: var(--bg);
    position: sticky; top: 0; z-index: 20;
  }
  .app-header::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -1px;
    height: 1px; background:
      repeating-linear-gradient(90deg, var(--line-strong) 0 4px, transparent 4px 8px);
    opacity: 0.5;
  }
  .h-left { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
  .h-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .h-eyebrow {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--fg-faint);
  }
  .h-title { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
  .h-right { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex-shrink: 0; }
  .h-update {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    color: var(--fg-dim); display: flex; align-items: center; gap: 6px;
    white-space: nowrap;
  }
  .h-update .live-dot {
    width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
    box-shadow: 0 0 6px var(--accent); animation: live 2s ease-in-out infinite;
  }
  .h-update.offline .live-dot { background: var(--fg-faint); box-shadow: none; animation: none; }
  @keyframes live { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
  .h-batt {
    display: flex; align-items: center; gap: 6px;
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    color: var(--fg-dim);
    white-space: nowrap; flex-wrap: nowrap;
  }
  .batt-svg {
    width: 22px; height: 11px; border: 1px solid var(--fg-dim); border-radius: 2px;
    position: relative; padding: 1px; box-sizing: border-box;
  }
  .batt-svg::after {
    content: ''; position: absolute; right: -3px; top: 3px; width: 2px; height: 5px;
    background: var(--fg-dim); border-radius: 0 1px 1px 0;
  }
  .batt-fill { height: 100%; background: var(--ok); }

  /* ── Main scroll ──────────────────────────────────────── */
  .app-main {
    flex: 1;
    -webkit-overflow-scrolling: touch;
  }

  .tab-content {
    padding: 14px 14px calc(110px + env(safe-area-inset-bottom));
    display: flex; flex-direction: column; gap: 12px;
  }

  /* ── Banner (fallback / error) ────────────────────────── */
  .banner {
    margin: 0 14px; margin-top: 12px;
    padding: 10px 12px;
    border-radius: 10px;
    border: 1px solid var(--line-strong);
    background: var(--bg-elev);
    color: var(--fg-dim);
    font-family: 'JetBrains Mono', monospace; font-size: 10.5px;
    line-height: 1.5;
    display: flex; gap: 10px; align-items: flex-start;
  }
  .banner .dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--warn); box-shadow: 0 0 6px var(--warn);
    flex-shrink: 0; margin-top: 5px;
  }
  .banner b { color: var(--fg); font-weight: 600; }

  .empty-state {
    text-align: center; padding: 60px 20px;
    color: var(--fg-dim);
  }
  .empty-state .empty-eyebrow {
    display: block;
    font-size: 10px; letter-spacing: 0.2em; color: var(--fg-faint);
    margin-bottom: 12px;
  }

  /* ── Hero ─────────────────────────────────────────────── */
  .hero {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 14px 14px 8px;
    position: relative;
    overflow: hidden;
  }
  .hero::before {
    content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    opacity: 0.6;
  }
  .hero-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 12px; margin-bottom: 8px;
  }
  .hero-eyebrow {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--fg-faint);
    display: block; margin-bottom: 6px;
  }
  .hero-title {
    margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.02em;
    line-height: 1.1;
  }
  .hero-prob { display: flex; align-items: baseline; gap: 1px; }
  .hero-prob.storm { color: var(--accent); }
  .hero-prob.clear { color: var(--cool); }
  .hero-prob-num { font-size: 38px; font-weight: 600; line-height: 1; letter-spacing: -0.04em; }
  .hero-prob-pct { font-size: 16px; font-family: 'JetBrains Mono', monospace; opacity: 0.7; }
  .hero-foot {
    display: flex; gap: 14px; padding-top: 10px; margin-top: 4px;
    border-top: 1px solid var(--line);
    flex-wrap: wrap;
  }
  .hero-meta { display: flex; flex-direction: column; gap: 2px; }
  .meta-label {
    font-family: 'JetBrains Mono', monospace; font-size: 8.5px;
    letter-spacing: 0.14em; color: var(--fg-faint);
  }
  .meta-val { font-size: 11px; color: var(--fg); }
  .meta-val.alert { color: var(--accent); }

  /* ── Sensor grid ──────────────────────────────────────── */
  .sensor-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
  }
  .metric-card {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 12px;
    display: flex; flex-direction: column;
    min-height: 96px;
    position: relative;
  }
  .metric-card.large { grid-column: 1 / -1; }
  .metric-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 10px;
  }
  .metric-label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9.5px; letter-spacing: 0.14em; color: var(--fg-faint);
  }
  .metric-trend {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    color: var(--fg-dim);
  }
  .metric-trend.up { color: var(--accent); }
  .metric-trend.down { color: var(--cool); }
  .metric-row {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: 12px; flex: 1;
  }
  .metric-card.large .metric-row { align-items: center; }
  .metric-value-wrap { display: flex; align-items: baseline; gap: 4px; }
  .metric-value {
    font-size: 28px; font-weight: 600; letter-spacing: -0.02em;
    line-height: 1;
  }
  .metric-card.large .metric-value { font-size: 56px; font-weight: 500; }
  .metric-unit {
    font-family: 'JetBrains Mono', monospace; font-size: 12px;
    color: var(--fg-faint);
  }
  .metric-spark { flex: 1; min-width: 50px; max-width: 80px; }
  .metric-card.large .metric-spark { max-width: 160px; height: 36px; }
  .metric-sub {
    margin-top: 8px;
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    color: var(--fg-faint);
    border-top: 1px dashed var(--line);
    padding-top: 8px;
  }

  /* ── Telemetry strip ──────────────────────────────────── */
  .telemetry {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 4px 12px;
  }
  .telemetry-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 9px 0;
    border-bottom: 1px solid var(--line);
  }
  .telemetry-row:last-child { border-bottom: 0; }
  .t-key {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    letter-spacing: 0.12em; color: var(--fg-faint);
  }
  .t-val { font-size: 12px; color: var(--fg); }

  /* ── GPS card ─────────────────────────────────────────── */
  .gps-card {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 12px;
  }
  .gps-head {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 10px;
  }
  .gps-eyebrow {
    font-family: 'JetBrains Mono', monospace; font-size: 9.5px;
    letter-spacing: 0.14em; color: var(--fg-faint);
  }
  .gps-sats { font-size: 10px; color: var(--cool); }
  .map-placeholder {
    width: 100%; height: 220px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 8px;
    overflow: hidden;
    margin-bottom: 10px;
  }
  .leaflet-container {
    background: var(--bg-elev-2);
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
  }
  .leaflet-control-attribution {
    background: var(--attribution-bg) !important;
    color: var(--fg-faint) !important;
    backdrop-filter: blur(4px);
    border-radius: 4px 0 0 0;
    padding: 2px 6px !important;
    font-size: 9px !important;
  }
  .leaflet-control-attribution a { color: var(--fg-dim) !important; }
  .leaflet-control-zoom a {
    background: var(--bg-elev) !important;
    color: var(--fg) !important;
    border-color: var(--line-strong) !important;
  }
  .leaflet-control-zoom a:hover {
    background: var(--bg-elev-2) !important;
  }
  .gps-coords { display: flex; flex-direction: column; gap: 6px; }
  .gps-row {
    display: flex; justify-content: space-between; align-items: center;
    font-size: 11px;
  }
  .gps-row > span:first-child {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    letter-spacing: 0.12em; color: var(--fg-faint);
  }

  /* ── Sensor health ────────────────────────────────────── */
  .health {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 12px;
  }
  .section-head {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 10px;
  }
  .section-title {
    font-family: 'JetBrains Mono', monospace; font-size: 9.5px;
    letter-spacing: 0.14em; color: var(--fg-faint);
    white-space: nowrap;
  }
  .section-sub { font-size: 10px; color: var(--fg-dim); white-space: nowrap; }
  .health-list { display: flex; flex-direction: column; gap: 6px; }
  .health-row {
    display: grid; grid-template-columns: 8px 60px 1fr auto;
    align-items: center; gap: 10px;
    padding: 6px 0;
    border-bottom: 1px dashed var(--line);
  }
  .health-row:last-child { border-bottom: 0; }
  .health-dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--ok);
    box-shadow: 0 0 6px var(--ok);
  }
  .health-dot.stale { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
  .health-id { font-size: 10px; color: var(--fg); letter-spacing: 0.04em; }
  .health-label { font-size: 11px; color: var(--fg-dim); }
  .health-age { font-size: 10px; color: var(--fg-faint); }

  /* ── Range bar (Charts tab) ───────────────────────────── */
  .range-bar {
    display: flex; gap: 4px; padding: 4px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 10px;
  }
  .range-btn {
    flex: 1; padding: 8px 0;
    border: 0; background: transparent;
    color: var(--fg-dim);
    font-family: 'JetBrains Mono', monospace; font-size: 11px;
    letter-spacing: 0.06em;
    border-radius: 7px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
  }
  .range-btn:hover { color: var(--fg); }
  .range-btn.active {
    background: var(--bg-elev-2);
    color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--line-strong);
  }

  /* ── Instrument chart panel ───────────────────────────── */
  .instr-chart {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 12px 4px 4px 4px;
  }
  .instr-chart-head {
    display: flex; justify-content: space-between; align-items: center;
    padding: 0 10px 8px;
  }
  .instr-chart-label {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    letter-spacing: 0.14em; color: var(--fg-faint);
  }
  .instr-chart-stats {
    display: flex; gap: 10px;
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    color: var(--fg-dim);
  }
  .instr-chart-stats em {
    font-style: normal; color: var(--fg-faint); margin-right: 3px;
    font-size: 9px; letter-spacing: 0.08em;
  }
  .instr-chart-stats .lv { color: var(--fg); }
  .instr-chart-stats .lv u { text-decoration: none; color: var(--fg-faint); margin-left: 2px; }

  /* ── Config tab ───────────────────────────────────────── */
  .config-card {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 14px;
  }
  .kv-list { display: flex; flex-direction: column; gap: 0; }
  .kv-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 8px 0;
    border-bottom: 1px dashed var(--line);
    gap: 12px;
  }
  .kv-row:last-child { border-bottom: 0; }
  .kv-row > span:first-child {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    letter-spacing: 0.12em; color: var(--fg-faint);
    white-space: nowrap; flex-shrink: 0;
  }
  .kv-row > span:last-child {
    font-size: 11px; color: var(--fg);
    white-space: nowrap;
    text-align: right;
  }

  .status-pill {
    font-family: 'JetBrains Mono', monospace; font-size: 9px;
    letter-spacing: 0.12em;
    padding: 3px 7px; border-radius: 4px;
  }
  .status-pill.ok { background: oklch(0.74 0.14 145 / 0.15); color: var(--ok); }
  .status-pill.stale { background: oklch(0.74 0.16 60 / 0.15); color: var(--warn); }

  .seg-control {
    display: flex; gap: 4px; padding: 4px;
    background: var(--bg-elev-2);
    border-radius: 8px;
    border: 1px solid var(--line);
  }
  .seg-btn {
    flex: 1; padding: 8px 4px;
    border: 0; background: transparent;
    color: var(--fg-dim);
    font-family: 'JetBrains Mono', monospace; font-size: 10.5px;
    letter-spacing: 0.04em;
    border-radius: 6px; cursor: pointer;
    transition: all 0.15s;
  }
  .seg-btn:hover { color: var(--fg); }
  .seg-btn.active {
    background: var(--bg-elev);
    color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--line-strong);
  }
  .config-help {
    margin: 10px 0 0;
    font-size: 11px; color: var(--fg-faint); line-height: 1.5;
  }

  .token-row { display: flex; gap: 8px; align-items: center; }
  .text-input {
    flex: 1; padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--fg);
    font-size: 12px;
    outline: none;
  }
  .text-input:focus { border-color: var(--line-strong); }
  .icon-btn {
    width: 38px; height: 38px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--fg-dim);
    cursor: pointer;
    font-size: 14px;
  }

  .toggle-row {
    display: flex; justify-content: space-between; align-items: center;
    cursor: pointer;
  }
  .toggle-info { display: flex; flex-direction: column; gap: 3px; }
  .toggle-label { font-size: 13px; color: var(--fg); }
  .toggle-sub { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--fg-faint); }
  .toggle-switch {
    width: 38px; height: 22px; border-radius: 999px;
    background: var(--line-strong);
    position: relative; transition: background 0.2s;
    flex-shrink: 0;
  }
  .toggle-switch i {
    position: absolute; top: 2px; left: 2px;
    width: 18px; height: 18px; border-radius: 50%;
    background: var(--switch-knob);
    transition: transform 0.2s;
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  }
  .toggle-switch.on { background: var(--accent); }
  .toggle-switch.on i { transform: translateX(16px); }

  .save-btn {
    width: 100%; padding: 14px;
    background: var(--accent);
    color: var(--save-btn-fg);
    border: 0; border-radius: 10px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px; font-weight: 600;
    letter-spacing: 0.12em;
    cursor: pointer;
    transition: opacity 0.15s;
  }
  .save-btn:hover { opacity: 0.9; }
  .save-btn.saving { opacity: 0.6; }
  .config-foot {
    text-align: center;
    font-size: 9.5px; color: var(--fg-faint);
    letter-spacing: 0.08em;
    padding: 8px 0;
  }

  /* ── Bottom tab nav ───────────────────────────────────── */
  .tab-nav {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(12px + env(safe-area-inset-bottom));
    width: calc(100% - 24px);
    max-width: 456px;
    display: flex;
    background: var(--bg-elev);
    border: 1px solid var(--line-strong);
    border-radius: 14px;
    padding: 5px;
    backdrop-filter: blur(20px);
    box-shadow: var(--shadow-nav);
    z-index: 30;
  }
  .tab-btn {
    flex: 1;
    border: 0; background: transparent;
    padding: 9px 0 8px;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    color: var(--fg-faint);
    cursor: pointer;
    border-radius: 10px;
    transition: all 0.15s;
    font-family: 'Inter', sans-serif;
  }
  .tab-btn:hover { color: var(--fg-dim); }
  .tab-btn.active { color: var(--accent); background: var(--bg-elev-2); }
  .tab-icon { font-family: 'JetBrains Mono', monospace; font-size: 14px; line-height: 1; font-weight: 500; }
  .tab-label {
    font-family: 'JetBrains Mono', monospace; font-size: 9px;
    letter-spacing: 0.1em; text-transform: uppercase;
  }

  /* ── Toast ────────────────────────────────────────────── */
  .toast {
    position: fixed;
    bottom: calc(90px + env(safe-area-inset-bottom));
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 16px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line-strong);
    border-radius: 10px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; color: var(--fg);
    box-shadow: var(--shadow-toast);
    opacity: 0; pointer-events: none;
    transition: opacity 0.2s, transform 0.2s;
    z-index: 40;
    width: max-content;
    max-width: min(90vw, 430px);
    text-align: center;
    line-height: 1.45;
  }
  .toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }

  /* ── AgroSense additions ──────────────────────────────── */
  .h-left .h-eyebrow { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .h-device {
    background: transparent; border: 0; padding: 0; color: var(--fg);
    font: inherit; font-size: 15px; font-weight: 600; letter-spacing: -0.01em;
    cursor: pointer; text-align: left; max-width: 100%;
  }
  select.h-device { appearance: none; -webkit-appearance: none; }

  .photo-hero {
    position: relative; border-radius: 14px; overflow: hidden;
    border: 1px solid var(--line); background: var(--bg-elev);
    aspect-ratio: 4 / 3; cursor: pointer;
  }
  .photo-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .photo-hero-cap {
    position: absolute; left: 0; right: 0; bottom: 0;
    display: flex; justify-content: space-between;
    padding: 18px 12px 9px; font-size: 10px; letter-spacing: 0.08em;
    color: #fff; background: linear-gradient(transparent, rgba(0,0,0,0.65));
  }
  .photo-hero-empty {
    height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 6px; color: var(--fg-faint); font-size: 11px; cursor: default;
    background: repeating-linear-gradient(135deg, var(--bg-elev) 0 10px, var(--bg-elev-2) 10px 20px);
  }
  .photo-hero:has(.photo-hero-empty) { cursor: default; aspect-ratio: 16 / 5; }

  .map-placeholder.dark .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.85) contrast(0.9); }

  .hero-note { margin: 8px 0 4px; font-size: 10.5px; color: var(--fg-faint); line-height: 1.45; }

  .compass-wrap { display: flex; flex-direction: column; align-items: center; gap: 2px; }
  .compass-lbl { font-size: 10px; color: var(--fg-dim); }

  .health-dot.bad { background: var(--fg-faint); box-shadow: none; outline: 1px solid var(--warn); }

  .instr-legend { display: inline-flex; gap: 8px; margin-left: 10px; letter-spacing: 0.06em; }
  .instr-legend span { display: inline-flex; align-items: center; gap: 4px; }
  .instr-legend i { width: 8px; height: 2px; display: inline-block; }

  .cam-head { margin: 2px 2px 0; }
  .photo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .photo-cell {
    padding: 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
    background: var(--bg-elev); cursor: pointer; display: flex; flex-direction: column; color: var(--fg-dim);
  }
  .photo-cell img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; background: var(--bg-elev-2); }
  .photo-cell span { font-size: 9.5px; padding: 6px 8px; text-align: left; letter-spacing: 0.04em; }
  .more-btn, .logout-btn {
    width: 100%; padding: 12px; border-radius: 10px; cursor: pointer;
    background: var(--bg-elev); color: var(--fg-dim); border: 1px solid var(--line-strong);
    font-size: 11px; letter-spacing: 0.12em;
  }
  .lightbox {
    position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,0.92);
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
    padding: 16px;
  }
  .lightbox img { max-width: 100%; max-height: calc(100vh - 110px); border-radius: 6px; }
  .lightbox-bar { display: flex; align-items: center; gap: 14px; color: #ddd; font-size: 11px; }
  .lightbox-bar button {
    width: 40px; height: 40px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.25);
    background: transparent; color: #fff; font-size: 18px; cursor: pointer;
  }
  .lightbox-bar button:disabled { opacity: 0.25; cursor: default; }

  .dl-row { display: flex; gap: 8px; }
  .dl-btn {
    flex: 1; text-align: center; padding: 11px 0; border-radius: 8px; text-decoration: none;
    border: 1px solid var(--line-strong); background: var(--bg-elev-2); color: var(--accent);
    font-size: 11px; letter-spacing: 0.1em;
  }
  .dl-btn.disabled { color: var(--fg-faint); pointer-events: none; }
  .save-btn { margin-top: 12px; }
  .save-btn:disabled { opacity: 0.55; cursor: default; }

  .gate { padding: 40px 18px; display: flex; flex-direction: column; gap: 14px; }
  .gate h1 { margin: 0; font-size: 24px; letter-spacing: -0.02em; }
  .gate p { margin: 0; color: var(--fg-dim); font-size: 13px; line-height: 1.55; }
  .gate .text-input { padding: 13px 12px; }
  .gate-err { color: var(--warn); font-size: 11px; }
  .link-btn {
    background: transparent; border: 0; color: var(--fg-dim); cursor: pointer;
    font-size: 11px; letter-spacing: 0.1em; padding: 10px;
  }
`;

const POLL_MS = 60 * 1000;
const HISTORY_DAYS = 30;            // the charts go up to 30 d
const PHOTO_DAYS = 7;

const THEME_KEY = 'agrosense_theme';
const THEME_VALUES = ['auto', 'light', 'dark'];
const DEVICE_KEY = 'agrosense_device';

function readStored(key, fallback) {
  try { return localStorage.getItem(key) || fallback; } catch { return fallback; }
}
function store(key, value) {
  try { if (value) localStorage.setItem(key, value); else localStorage.removeItem(key); } catch { /* private mode */ }
}

function TokenGate({ onLogin, onDemo }) {
  const [token, setToken] = useState('');
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);
  const submit = async (e) => {
    e.preventDefault();
    setBusy(true);
    setErr(null);
    window.AS_API.setToken(token.trim());
    try {
      const devices = await window.AS_API.fetchDevices();
      onLogin(devices);
    } catch (ex) {
      window.AS_API.setToken('');
      setErr(ex instanceof window.AS_API.AuthError ? 'Token não aceito.' : `Erro: ${ex.message}`);
    } finally {
      setBusy(false);
    }
  };
  return (
    <form className="gate" onSubmit={submit}>
      <span className="h-eyebrow">FAPERJ · Sumidouro/RJ</span>
      <h1>AgroSense</h1>
      <p>Painel das estações AgroSense v3. Os dados são de pesquisa: entre com o token do painel.</p>
      <input className="text-input mono" type="password" autoFocus value={token}
        onChange={(e) => setToken(e.target.value)} placeholder="token" autoComplete="current-password" />
      {err && <span className="gate-err mono">{err}</span>}
      <button className="save-btn" type="submit" disabled={busy || !token.trim()}>{busy ? 'CONFERINDO…' : 'ENTRAR'}</button>
      <button className="link-btn mono" type="button" onClick={onDemo}>VER DEMONSTRAÇÃO</button>
    </form>
  );
}

function App() {
  const [tab, setTab] = useState('weather');
  const [toast, setToast] = useState(null);
  const [tickNow, setTickNow] = useState(Date.now());
  const [theme, setThemeState] = useState(() => {
    const v = readStored(THEME_KEY, 'auto');
    return THEME_VALUES.includes(v) ? v : 'auto';
  });
  const [systemDark, setSystemDark] = useState(() =>
    window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)').matches : true);

  // 'gate' | 'live' | 'demo'
  const [mode, setMode] = useState(window.AS_API.getToken() ? 'live' : 'gate');
  const [devices, setDevices] = useState([]);
  const [deviceId, setDeviceId] = useState(readStored(DEVICE_KEY, ''));
  const [history, setHistory] = useState([]);
  const [photos, setPhotos] = useState([]);
  const [loading, setLoading] = useState(false);
  const [apiError, setApiError] = useState(null);

  useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    const onChange = (e) => setSystemDark(e.matches);
    if (mq.addEventListener) mq.addEventListener('change', onChange); else mq.addListener(onChange);
    return () => { if (mq.removeEventListener) mq.removeEventListener('change', onChange); else mq.removeListener(onChange); };
  }, []);

  const isDark = theme === 'auto' ? systemDark : theme === 'dark';
  useEffect(() => {
    document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
    const meta = document.querySelector('meta[name="theme-color"]');
    if (meta) meta.setAttribute('content', isDark ? '#0b0d10' : '#f4f4f0');
  }, [isDark]);

  const setTheme = useCallback((v) => {
    if (!THEME_VALUES.includes(v)) return;
    setThemeState(v);
    store(THEME_KEY, v === 'auto' ? '' : v);
  }, []);

  useEffect(() => {
    const id = window.setInterval(() => setTickNow(Date.now()), 5000);
    return () => window.clearInterval(id);
  }, []);

  const showToast = (msg) => {
    setToast(msg);
    window.setTimeout(() => setToast(null), 3000);
  };

  const logout = () => {
    window.AS_API.setToken('');
    setMode('gate');
    setDevices([]); setHistory([]); setPhotos([]);
  };

  const handleError = (err) => {
    if (err instanceof window.AS_API.AuthError) logout();
    else setApiError(err.message);
  };

  // ── Devices ─────────────────────────────────────────────
  const loadDevices = useCallback(async () => {
    try {
      const list = await window.AS_API.fetchDevices();
      setDevices(list);
      setApiError(null);
      return list;
    } catch (err) { handleError(err); return null; }
  }, []);

  useEffect(() => { if (mode === 'live') loadDevices(); }, [mode, loadDevices]);
  useEffect(() => setTab('weather'), [mode]);

  // Default station: the stored one, else the one that spoke most recently
  useEffect(() => {
    if (!devices.length) return;
    if (!devices.some(d => d.device_id === deviceId)) {
      const best = [...devices].sort((a, b) => (b.last_seen || 0) - (a.last_seen || 0))[0];
      setDeviceId(best.device_id);
    }
  }, [devices]);

  useEffect(() => { if (deviceId) store(DEVICE_KEY, deviceId); }, [deviceId]);

  // ── Readings + photos: full load, then only what's new ───
  useEffect(() => {
    if (mode !== 'live' || !deviceId) return;
    let cancelled = false;
    let rows = [];
    const now = () => Math.floor(Date.now() / 1000);

    const full = async () => {
      setLoading(true);
      try {
        const [r, p] = await Promise.all([
          window.AS_API.fetchReadings(deviceId, now() - HISTORY_DAYS * 86400),
          window.AS_API.fetchPhotos(deviceId, now() - PHOTO_DAYS * 86400),
        ]);
        if (cancelled) return;
        rows = r.filter(x => x.ts != null).sort((a, b) => a.ts - b.ts);
        setHistory(rows);
        setPhotos(p);
        setApiError(null);
      } catch (err) { if (!cancelled) handleError(err); }
      finally { if (!cancelled) setLoading(false); }
    };

    const poll = async () => {
      try {
        const since = rows.length ? rows[rows.length - 1].ts + 1 : now() - HISTORY_DAYS * 86400;
        const r = await window.AS_API.fetchReadings(deviceId, since);
        if (cancelled || !r.length) return;
        // The station uploads its backlog oldest first, so new rows can predate the
        // newest one we have: merge by seq, keep time order, drop the old tail.
        const bySeq = new Map(rows.map(x => [x.seq, x]));
        r.forEach(x => x.ts != null && bySeq.set(x.seq, x));
        const cutoff = now() - HISTORY_DAYS * 86400;
        rows = [...bySeq.values()].filter(x => x.ts >= cutoff).sort((a, b) => a.ts - b.ts);
        setHistory(rows);
        if (r.some(x => x.photo)) setPhotos(await window.AS_API.fetchPhotos(deviceId, now() - PHOTO_DAYS * 86400));
        loadDevices();
      } catch (err) { if (!cancelled) handleError(err); }
    };

    setHistory([]); setPhotos([]);
    full();
    const id = window.setInterval(poll, POLL_MS);
    return () => { cancelled = true; window.clearInterval(id); };
  }, [mode, deviceId]);

  const demoHistory = useMemo(() => (mode === 'demo' ? window.generateDemo(HISTORY_DAYS * 24) : []), [mode]);

  if (mode === 'gate') {
    return <div className="app"><TokenGate onLogin={(list) => { setDevices(list); setMode('live'); }} onDemo={() => setMode('demo')} /></div>;
  }

  const demo = mode === 'demo';
  const effHistory = demo ? demoHistory : history;
  const latest = effHistory.length ? effHistory[effHistory.length - 1] : null;
  const device = demo
    ? { device_id: 'agrosense-01', description: 'demonstração', readings: effHistory.length, photos: 0, measure_interval_minutes: 5 }
    : devices.find(d => d.device_id === deviceId);
  const nowSec = Math.floor(tickNow / 1000);
  const lastPhoto = photos.length ? photos[photos.length - 1] : null;

  const age = latest ? nowSec - latest.ts : null;
  const lastUpdateAgo = age == null ? '—' : age < 60 ? `há ${age}s` : age < 3600 ? `há ${Math.round(age / 60)}min`
    : age < 86400 * 2 ? `há ${Math.round(age / 3600)}h` : `há ${Math.round(age / 86400)}d`;
  const offline = age == null || age > 30 * 60;
  const battPct = latest && Number.isFinite(latest.battery_percent) ? Math.round(latest.battery_percent) : null;
  const curId = demo ? 'agrosense-01' : deviceId;

  return (
    <div className="app">
      <header className="app-header">
        <div className="h-left">
          <span className="h-eyebrow">AgroSense v3 · {demo ? 'demonstração' : (device?.description || 'estação')}</span>
          {!demo && devices.length > 1 ? (
            <select className="h-device h-title" value={deviceId} onChange={(e) => setDeviceId(e.target.value)}>
              {devices.map(d => <option key={d.device_id} value={d.device_id}>{d.device_id} ▾</option>)}
            </select>
          ) : (
            <span className="h-title mono">{curId || '—'}</span>
          )}
        </div>
        <div className="h-right">
          <span className={`h-update ${offline ? 'offline' : ''}`}><span className="live-dot" />{lastUpdateAgo}</span>
          <span className="h-batt">
            <span className="batt-svg"><span className="batt-fill" style={{ width: `${battPct ?? 0}%` }} /></span>
            {battPct == null ? '—' : `${battPct}%`}
          </span>
        </div>
      </header>

      {demo && (
        <div className="banner">
          <span className="dot" />
          <span><b>Demonstração</b> — dados sintéticos de uma semana típica na serra. Os dados reais pedem o token do painel.</span>
        </div>
      )}
      {!demo && apiError && (
        <div className="banner">
          <span className="dot" />
          <span><b>Sem conexão com a API</b> — mostrando o que já foi carregado.<br />Erro: <span className="mono">{apiError}</span></span>
        </div>
      )}
      {!demo && loading && !history.length && (
        <div className="banner"><span className="dot" /><span>Carregando o dataset…</span></div>
      )}

      <main className="app-main">
        {tab === 'weather' && (
          <WeatherTab history={effHistory} latest={latest} dark={isDark} deviceId={curId} demo={demo}
            lastPhoto={demo ? null : lastPhoto} nowSec={nowSec} onOpenCamera={() => setTab('camera')} />
        )}
        {tab === 'charts' && <ChartsTab history={effHistory} dark={isDark} nowSec={latest ? Math.max(latest.ts, nowSec - 3600) : nowSec} />}
        {tab === 'camera' && <CameraTab deviceId={curId} photos={demo ? [] : photos} demo={demo} loading={loading} />}
        {tab === 'station' && (
          <StationTab device={device} latest={latest} demo={demo} onToast={showToast} onIntervalSaved={loadDevices}
            theme={theme} onThemeChange={setTheme} dark={isDark} nowSec={nowSec}
            onLogout={logout} />
        )}
      </main>

      <div className={`toast ${toast ? 'show' : ''}`}>{toast || ''}</div>

      <nav className="tab-nav">
        {[
          { k: 'weather', i: '◐', l: 'Tempo' },
          { k: 'charts', i: '◰', l: 'Gráficos' },
          { k: 'camera', i: '◉', l: 'Câmera' },
          { k: 'station', i: '◇', l: 'Estação' },
        ].map(t => (
          <button key={t.k} className={`tab-btn ${tab === t.k ? 'active' : ''}`} onClick={() => setTab(t.k)}>
            <span className="tab-icon">{t.i}</span>
            <span className="tab-label">{t.l}</span>
          </button>
        ))}
      </nav>
    </div>
  );
}

const styleEl = document.createElement('style');
styleEl.textContent = APP_CSS;
document.head.appendChild(styleEl);

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
