:root{
  --bg:#0a0e1a; --bg2:#0e1530;
  --card:rgba(255,255,255,.045); --card-brd:rgba(255,255,255,.08);
  --text:#e7ecff; --muted:#8b93b0;
  --solar:#ffb020; --solar2:#ff7a00;
  --soc:#37e0a0; --volt:#4aa3ff; --charge:#8b7cff; --load:#ff5d73;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
html{background:#0a0e1a}   /* fill the iOS status-bar / overscroll area (was showing white) */
body{
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif;
  background:radial-gradient(1200px 700px at 70% -10%, #16204a 0%, var(--bg2) 35%, var(--bg) 75%);
  /* dvh (not vh) so the body floor matches the *visible* iOS viewport; 100vh is the
     large viewport and left ~a URL-bar's worth of phantom scroll when content fit */
  color:var(--text); min-height:100vh; min-height:100dvh; -webkit-font-smoothing:antialiased;
}
.bg-glow{position:fixed;inset:0;pointer-events:none;z-index:0;
  background:radial-gradient(600px 400px at 15% 0%, rgba(255,176,32,.10), transparent 60%),
             radial-gradient(700px 500px at 90% 20%, rgba(74,163,255,.08), transparent 60%);}
main{position:relative;z-index:1;max-width:1560px;margin:0 auto;padding:0 20px 12px}
html.lockscroll,body.lockscroll{overflow:hidden}
/* the fit-to-one-screen lock is desktop-only; below 900px the Power/Detailed views
   are taller than the viewport, so never trap scrolling (was hiding content behind the URL bar) */
@media(max-width:899px){ html.lockscroll,body.lockscroll{overflow:visible} }

/* topbar */
.topbar{position:relative;z-index:1;max-width:1560px;margin:0 auto;
  padding:calc(18px + env(safe-area-inset-top)) 20px 10px;
  display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:12px}
.brand{display:flex;align-items:center;gap:12px;justify-self:start}
.status{justify-self:end}
.logo{display:flex;filter:drop-shadow(0 0 14px rgba(255,176,32,.55))}
.logo svg{width:34px;height:34px;display:block}
.brand-name{font-size:19px;font-weight:750;letter-spacing:.2px;white-space:nowrap;line-height:1.1}
.brand-name span{color:var(--solar);font-weight:700}
.status{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--muted);
  background:var(--card);border:1px solid var(--card-brd);padding:7px 14px;border-radius:999px}
.status .countdown{font-variant-numeric:tabular-nums;min-width:74px;text-align:right}

/* view toggle */
.viewtoggle{display:inline-flex;gap:4px;background:var(--card);border:1px solid var(--card-brd);
  padding:5px;border-radius:12px}
.viewtoggle button{background:transparent;border:0;color:var(--muted);font-weight:650;font-size:13px;
  padding:7px 18px;border-radius:9px;cursor:pointer;transition:.25s ease;letter-spacing:.4px}
.viewtoggle button:hover{color:var(--text)}
.viewtoggle button.active{background:linear-gradient(180deg,rgba(74,163,255,.30),rgba(74,163,255,.16));
  color:#fff;box-shadow:0 2px 12px rgba(74,163,255,.25)}

/* narrow screens: the brand + 5 tabs + 4 status dots can't share one row — stack them,
   and let the tab strip / status strip scroll horizontally instead of overflowing the page */
@media(max-width:1140px){
  .topbar{grid-template-columns:1fr;justify-items:center;gap:10px;
    padding:calc(12px + env(safe-area-inset-top)) 12px 8px}
  .brand,.status{justify-self:center}
  /* wrap tabs + status onto multiple centered rows instead of clipping them */
  .viewtoggle,.status{max-width:100%;flex-wrap:wrap;justify-content:center;row-gap:6px}
  .viewtoggle button{padding:7px 13px}
}

/* views */
.view{display:none}
.view.active{display:block;animation:fade .45s cubic-bezier(.33,0,.15,1)}
@keyframes fade{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}

/* ===== In-development placeholder (Environmental / Security) ===== */
#view-environmental.active,#view-security.active{display:flex}

/* ===== BASIC VIEW — the battery is the star; fills the screen, no scroll ===== */
#view-basic.active{display:flex;flex-direction:column;gap:16px;
  height:calc(100dvh - var(--header-h,66px) - 14px);min-height:360px;padding-top:4px}
.batt-hero{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center}
.batt-gauge{display:none}   /* shown instead of the battery on phones (see mobile media query) */
.batt-shell{display:flex;align-items:center;width:min(76%,780px);height:min(42vh,320px)}
.batt-body{position:relative;flex:1;height:100%;border:6px solid rgba(255,255,255,.16);
  border-radius:28px;overflow:hidden;background:rgba(255,255,255,.03);
  box-shadow:inset 0 2px 30px rgba(0,0,0,.35)}
.batt-fill{position:absolute;left:0;top:0;bottom:0;width:0;border-radius:22px 0 0 22px;
  transition:width 1.5s cubic-bezier(.33,0,.15,1), background 1.5s ease-in-out}
.batt-cells{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:repeating-linear-gradient(90deg,transparent 0 calc(20% - 2px),rgba(0,0,0,.28) calc(20% - 2px) 20%)}
.batt-center{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;
  align-items:center;justify-content:center;text-shadow:0 2px 16px rgba(0,0,0,.55)}
.batt-pct{font-size:clamp(48px,10vw,118px);font-weight:820;letter-spacing:-3px;line-height:.9;color:#fff;
  font-variant-numeric:tabular-nums}   /* fixed-width digits so the rolling thousandths don't jitter */
.batt-pct span{font-size:.36em;font-weight:700;color:rgba(255,255,255,.7);margin-left:8px;letter-spacing:0}
.batt-cap{width:26px;height:36%;background:rgba(255,255,255,.16);border-radius:0 12px 12px 0;margin-left:7px}

/* power-flow dots at the battery terminal: stream in when charging, out when discharging */
.pflow{position:relative;flex:0 0 auto;width:92px;height:100%;margin-left:6px;overflow:hidden;--flow-dist:84px}
.pdot{position:absolute;top:50%;left:0;width:10px;height:10px;margin-top:-5px;border-radius:50%;
  opacity:0;animation-duration:var(--flow-dur,2s);animation-timing-function:linear;
  animation-iteration-count:infinite}
.pflow.charging .pdot{animation-name:flowIn;background:var(--soc);box-shadow:0 0 9px var(--soc)}
.pflow.discharging .pdot{animation-name:flowOut;background:var(--load);box-shadow:0 0 9px var(--load)}
.pflow.idle .pdot{animation:none;opacity:0}
.pdot:nth-child(1){animation-delay:calc(var(--flow-dur,2s) * 0)}
.pdot:nth-child(2){animation-delay:calc(var(--flow-dur,2s) * .2)}
.pdot:nth-child(3){animation-delay:calc(var(--flow-dur,2s) * .4)}
.pdot:nth-child(4){animation-delay:calc(var(--flow-dur,2s) * .6)}
.pdot:nth-child(5){animation-delay:calc(var(--flow-dur,2s) * .8)}
@keyframes flowIn{0%{transform:translateX(var(--flow-dist,84px));opacity:0}14%{opacity:1}86%{opacity:1}100%{transform:translateX(0);opacity:0}}
@keyframes flowOut{0%{transform:translateX(0);opacity:0}14%{opacity:1}86%{opacity:1}100%{transform:translateX(var(--flow-dist,84px));opacity:0}}
@media(prefers-reduced-motion:reduce){.pdot{animation:none!important;opacity:.6}}

.basic-stats{flex:0 0 auto;display:grid;grid-template-columns:repeat(5,1fr);gap:16px}
/* grid blowout guard: without min-width:0 the cards refuse to shrink below their
   content width, and the 5-column row overflows into a horizontal scrollbar */
.basic-stats > .bstat{min-width:0}
.temp-mini{min-width:0}
.bstat{background:var(--card);border:1px solid var(--card-brd);border-radius:18px;
  padding:16px 14px;text-align:center;position:relative;overflow:hidden}
.bstat-ico{font-size:22px}
.bstat-val{font-size:clamp(24px,3vw,34px);font-weight:780;margin-top:4px;letter-spacing:-1px}
.bstat-val span{font-size:.45em;color:var(--muted);font-weight:600;margin-left:3px}
.bstat-lab{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-top:5px;font-weight:600}
.rt-note{font-size:.4em;font-weight:600;color:var(--muted);letter-spacing:0;white-space:nowrap}
.temp-mini-list.flow-mini{grid-template-columns:repeat(2,1fr)}
.temp-mini-list.flow-mini.solo{grid-template-columns:1fr}
.flow-mini.solo .temp-mini{border-left:none}
.flow-mini .temp-mini{justify-content:center}
.flow-mini .v span{font-size:.45em;color:var(--muted);font-weight:600;margin-left:2px}
.flow-split{margin-top:6px;font-size:11.5px;font-weight:600;color:var(--muted);
  letter-spacing:.3px;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
.bstat-temps{display:flex;flex-direction:column;justify-content:center}
.bstat-temps .bstat-lab{text-align:center;margin:0 0 10px}
.temp-mini-list{display:grid;grid-template-columns:repeat(3,1fr)}
.temp-mini{display:flex;flex-direction:column;align-items:center;gap:3px;padding:0 4px}
.temp-mini + .temp-mini{border-left:1px solid rgba(255,255,255,.14)}
.temp-mini .n{color:var(--muted);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px}
.temp-mini .v{font-weight:780;font-size:clamp(19px,2.4vw,28px);letter-spacing:-1px;line-height:1}
.temp-mini .v.g{color:var(--soc)} .temp-mini .v.y{color:#ffd23f} .temp-mini .v.r{color:var(--load)}
/* offline sensor: no stale number, just a pulsing red dot stacked over the text.
   min-height matches .temp-mini .v so an offline tile stays the same height as
   its neighbours; the dot keeps its own margin so the pulse ring isn't clipped. */
.v-off{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
  min-height:clamp(19px,2.4vw,28px);font-size:12.5px;font-weight:700;
  letter-spacing:.2px;color:var(--load);line-height:1}
.odot{width:8px;height:8px;border-radius:50%;background:var(--load);flex:0 0 auto;
  margin-top:3px;animation:offpulse 1.7s ease-out infinite}
@keyframes offpulse{
  0%{box-shadow:0 0 0 0 rgba(255,93,115,.65)}
  70%{box-shadow:0 0 0 7px rgba(255,93,115,0)}
  100%{box-shadow:0 0 0 0 rgba(255,93,115,0)}
}
@media(prefers-reduced-motion:reduce){ .odot{animation:none} }
/* offline overlay for chart-based displays (gauges, sparklines, mobile battery):
   sits over the chart rather than replacing it, so the ECharts instance bound to
   the container stays intact for when the sensor comes back. */
.sens-ovl{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:7px;border-radius:inherit;
  background:rgba(10,14,26,.62);backdrop-filter:blur(2px);
  font-size:12.5px;font-weight:700;letter-spacing:.2px;color:var(--load)}
.gauge-card,.metric-card,.batt-gauge{position:relative}
@media(max-width:620px){
  /* content-height flow (no forced stretch) so nothing balloons a gap between sections */
  #view-basic.active{height:auto;min-height:0;gap:14px;padding-top:6px}
  /* phones: swap the battery for a needle gauge (easier to read at a glance) */
  .batt-hero{display:none}
  .batt-gauge{display:block;flex:0 0 auto;width:100%}
  /* arc left, value right: the % inside the compact arc was cramped */
  .batt-gauge{display:flex;align-items:center;gap:4px}
  .batt-gauge .chart{flex:1 1 58%;min-width:0;height:min(26vh,195px)}
  /* fixed-width column: as the count-up changes digit count, a content-sized
     column would resize the chart beside it and make the arc jitter */
  .gauge-side{flex:0 0 138px;padding-right:10px;text-align:center}
  /* sized so the widest case ("100.0%") fits the 138px column on one line —
     at the old 46px it wrapped when crossing 99->100 and shifted the arc */
  .gv{font-size:clamp(26px,8.8vw,33px);font-weight:800;letter-spacing:-1px;line-height:1;
    color:#fff;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden}
  .gv.g{color:var(--soc)} .gv.y{color:#ffd23f} .gv.r{color:var(--load)}
  .gl{margin-top:6px;color:var(--muted);font-size:12px;font-weight:600;
    text-transform:uppercase;letter-spacing:1px}
  /* simple single-value cards share a row; the two wide cards (Power Flow, Temps) go full width */
  .basic-stats{grid-template-columns:repeat(2,1fr)}
  #bFlowCard{order:3;grid-column:1 / -1}
  .bstat-temps{order:4;grid-column:1 / -1}
  /* drop the whole status strip (sensor dots + refresh countdown) on phones */
  .status{display:none}
  /* phones show only the Power view — hide the tab bar (default route is Power) */
  .viewtoggle{display:none}
  /* smaller wordmark on phones */
  .brand{gap:8px}
  .brand-name{font-size:14px}
  .logo svg{width:24px;height:24px}
}
@media(max-width:640px){ main{padding:0 12px calc(20px + env(safe-area-inset-bottom))} }

/* ===== ADVANCED VIEW — cockpit grid ===== */
.adv-grid{display:flex;flex-direction:column;gap:12px}
.card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:2px}
.ranges.mini{padding:3px;border-radius:10px}
.ranges.mini button{padding:4px 8px;font-size:11px;letter-spacing:.3px}
.adv-stats{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:12px}
.adv-gauges{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.adv-gauges .chart.gauge{height:200px}

/* sparkline metric cards — shown instead of the big gauges on phones/tablets (<900) */
.adv-metrics{display:none}
.metric-card{background:var(--card);border:1px solid var(--card-brd);border-radius:16px;
  padding:12px 14px 8px;display:flex;flex-direction:column;gap:5px;overflow:hidden}
.metric-head{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--muted);
  text-transform:uppercase;letter-spacing:.6px;font-weight:600}
.metric-ico{font-size:15px}
.metric-val{font-size:clamp(22px,6vw,28px);font-weight:790;letter-spacing:-1px;line-height:1;color:#fff}
.metric-val .u{font-size:.5em;color:var(--muted);font-weight:600;margin-left:2px}
.m-solar .metric-val{color:var(--solar)}
.m-volt .metric-val{color:var(--volt)}
.mval.pos{color:var(--soc)} .mval.neg{color:var(--load)}
.spark{width:100%;height:44px;margin-top:2px}
@media(max-width:899px){
  .adv-gauges{display:none}
  .adv-metrics{display:grid;grid-template-columns:1fr 1fr;gap:12px}
}
.adv-charts{display:flex;flex-direction:column;gap:10px}
.adv-charts .ranges{align-self:center}
.adv-charts .chart{height:230px}
.adv-stats .tile{padding:10px 14px}
.adv-stats .tile-value{font-size:24px}
.tile.batt .tile-value{font-variant-numeric:tabular-nums}   /* steady width as the SoC thousandths roll */
.adv-stats .tile-foot{font-size:11.5px}
.card.adv-chart-card{padding:10px 12px 6px 14px}   /* .card is declared later; needs the extra class to win */
.adv-temps .temp-tile{padding:11px 16px}
.adv-temps .temp-val{font-size:28px}
/* combined relay card spans the full temps-strip width on its own row */
.temps-strip .temp-tile.rly{grid-column:1 / -1;display:flex;flex-direction:column;justify-content:center}
.relay-mini{margin-top:8px}
.relay-mini .temp-mini .v{font-size:clamp(20px,2.4vw,26px)}
.tile.load::before{background:linear-gradient(90deg,var(--load),#ff9aa9)}
.tile.load .tile-value{color:var(--load)}
@media(min-width:900px){
  #view-advanced.active{height:calc(100dvh - var(--header-h,66px) - 14px)}
  .adv-grid{height:100%;display:grid;gap:12px;
    grid-template-columns:1.05fr 1.4fr;
    grid-template-rows:auto 1fr auto;
    grid-template-areas:"stats stats" "gauges charts" "temps temps"}
  .adv-stats{grid-area:stats;grid-template-columns:repeat(5,1fr);margin:0}
  .adv-gauges{grid-area:gauges;grid-template-rows:1fr 1fr;min-height:0}
  .adv-gauges .gauge-card{display:flex;min-height:0}
  .adv-gauges .chart.gauge{flex:1 1 auto;height:auto;min-height:0;width:100%}
  .adv-charts{grid-area:charts;min-height:0}
  .adv-charts .adv-chart-card{flex:1 1 0;min-height:0;display:flex;flex-direction:column}
  .adv-charts .adv-chart-card .chart{flex:1 1 auto;height:auto;min-height:0}
  .adv-temps{grid-area:temps}
}

/* temperature tiles */
.temps-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px}
.temp-tile{background:var(--card);border:1px solid var(--card-brd);border-radius:16px;padding:16px 18px;
  position:relative;overflow:hidden}
.temp-tile::before{content:"";position:absolute;inset:0 0 auto 0;height:3px;background:var(--muted);transition:background .6s ease}
.temp-val,.temp-mini .v,.bstat-val{transition:color .6s ease}
/* status coloring by temperature: green <78°F, yellow 78–89, red 90+ */
.temp-tile.temp-green::before{background:var(--soc)}
.temp-tile.temp-yellow::before{background:#ffd23f}
.temp-tile.temp-red::before{background:var(--load)}
.temp-tile.temp-green .temp-val{color:var(--soc)}
.temp-tile.temp-yellow .temp-val{color:#ffd23f}
.temp-tile.temp-red .temp-val{color:var(--load)}
.temp-name{font-size:12.5px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:600}
.temp-val{font-size:34px;font-weight:760;margin-top:6px;letter-spacing:-1px}
.temp-val .unit{font-size:15px;font-weight:600;color:var(--muted);margin-left:3px}
.temp-sub{font-size:12.5px;color:var(--muted);margin-top:5px}
/* per-tile 12h sparkline; colour tracks the tile's temperature band */
.temp-spark{display:block;width:100%;height:26px;margin-top:8px}
div.temp-spark{min-height:26px}                 /* placeholder keeps height stable pre-data */
.temp-spark path{fill:none;stroke:var(--muted);stroke-width:1.6;
  vector-effect:non-scaling-stroke;opacity:.85;stroke-linejoin:round;stroke-linecap:round}
.temp-tile.temp-green .temp-spark path{stroke:var(--soc)}
.temp-tile.temp-yellow .temp-spark path{stroke:#ffd23f}
.temp-tile.temp-red .temp-spark path{stroke:var(--load)}
.temp-tile.stale{opacity:.55}
.temp-sub .warn{color:var(--load);font-weight:600}
/* offline sensor tile: replaces the value+sub rows; min-height matches their
   combined space (34px value line + sub line) so tiles stay uniform height */
.temp-off{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;
  gap:8px;margin-top:6px;min-height:61px;
  font-size:12.5px;font-weight:700;letter-spacing:.2px;color:var(--load)}
.temp-off .odot{margin:0 0 0 2px}
/* powered down (master relay off) = deliberate, not a fault: gray, no pulse */
.temp-off.pd,.v-off.pd{color:var(--muted)}
.temp-off.pd .odot,.v-off.pd .odot{background:var(--muted);animation:none}
/* relay status: green ON / muted OFF; multi-channel modules render one square per channel */
.temp-mini .v.o{color:var(--muted)}
.rdot{display:inline-block;width:11px;height:11px;border-radius:3px;
  background:rgba(255,255,255,.14);margin:0 4px 0 0;vertical-align:middle}
.rdot.on{background:var(--soc)}
.rdot.big{width:15px;height:15px;margin-right:6px}
.temp-tile.rly .temp-val .g{color:var(--soc)}
.temp-tile.rly .temp-val .o{color:var(--muted)}
.v.rdots{display:inline-flex;align-items:center;min-height:1em}
@media(max-width:640px){ .temps-strip{grid-template-columns:1fr} }

/* connection view */
.conn-head{display:flex;align-items:baseline;gap:14px;margin-top:8px}
.conn-title{font-size:18px;font-weight:750;letter-spacing:.2px}
.conn-sub{color:var(--muted);font-size:13px;font-weight:600}
.conn-group-title{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;
  letter-spacing:1px;margin:20px 0 10px}
.conn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}
/* reporting-server vitals */
.conn-server{display:grid;grid-template-columns:repeat(auto-fit,minmax(165px,1fr));gap:12px}
.srv-tile{background:var(--card);border:1px solid var(--card-brd);border-radius:12px;padding:12px 14px}
.srv-lab{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.srv-val{font-size:20px;font-weight:780;margin-top:5px;letter-spacing:-.5px}
.srv-val span{font-size:12px;font-weight:600;color:var(--muted)}
.srv-val.g{color:var(--soc)} .srv-val.y{color:#ffd23f} .srv-val.r{color:var(--load)}
.srv-sub{font-size:11.5px;color:var(--muted);margin-top:5px;font-weight:600}
.srv-bar span.y{background:#ffd23f} .srv-bar span.r{background:var(--load)}
.srv-spark{display:block;width:100%;height:22px;margin-top:8px}
.srv-spark path{stroke-width:1.5;vector-effect:non-scaling-stroke;opacity:.85;stroke:var(--soc)}
.srv-spark.sp-y path{stroke:#ffd23f} .srv-spark.sp-r path{stroke:var(--load)}
div.srv-spark{min-height:22px}   /* placeholder keeps tile height stable before data loads */
.conn-tile{background:var(--card);border:1px solid var(--card-brd);border-radius:14px;
  padding:14px 16px;position:relative;overflow:hidden}
.conn-tile::before{content:"";position:absolute;inset:0 0 auto 0;height:3px}
.conn-tile.up::before{background:var(--soc)} .conn-tile.down::before{background:var(--load)}
.conn-dot{position:absolute;top:12px;right:14px}
.conn-tile.up .conn-dot .odot{background:var(--soc);animation:none}
.conn-tile.down .conn-dot .odot{background:var(--load)}   /* red pulse via base .odot */
.conn-name{font-size:15px;font-weight:700;letter-spacing:-.2px;padding-right:16px}
.conn-src-sub{font-size:11.5px;color:var(--muted);margin-top:3px}
.conn-ms{margin-top:8px;font-size:13px;font-weight:700}
.conn-tile.up .conn-ms{color:var(--soc)} .conn-tile.down .conn-ms{color:var(--load)}
/* collapsed "Powered Off" section (relay off): calm, pulsing grey — not a fault */
.conn-poweroff{display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  background:var(--card);border:1px solid var(--card-brd);border-radius:14px;padding:14px 16px}
.conn-poweroff .odot.po{width:9px;height:9px;background:var(--muted);flex:0 0 auto;
  animation:offpulse-grey 1.7s ease-out infinite}
.conn-poweroff .po-lab{font-weight:700;color:var(--muted);letter-spacing:.2px}
.conn-poweroff .po-list{color:var(--muted);opacity:.7;font-size:12.5px}
@keyframes offpulse-grey{
  0%{box-shadow:0 0 0 0 rgba(139,147,176,.6)}
  70%{box-shadow:0 0 0 8px rgba(139,147,176,0)}
  100%{box-shadow:0 0 0 0 rgba(139,147,176,0)}
}
@media(prefers-reduced-motion:reduce){ .conn-poweroff .odot.po{animation:none} }
.conn-spark{display:block;width:100%;height:26px;margin-top:8px}
.conn-spark .cs-line{stroke:var(--soc);stroke-width:1.5;vector-effect:non-scaling-stroke;opacity:.85}
.conn-tile.down .conn-spark .cs-line{stroke:var(--muted)}
.conn-spark .cs-down rect{fill:var(--load)}
.conn-spark:empty,div.conn-spark{min-height:26px}   /* keep tile height stable before data loads */

/* sensors view */
.sensor-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(178px,1fr));gap:14px;margin-top:12px}
.sensor-section-title{font-size:15px;font-weight:750;letter-spacing:.3px;margin-top:22px}
.sensor-section-title .hint{font-weight:500;color:var(--muted);font-size:12px;margin-left:4px}
.chart.tall{height:340px}
.runtime.discharging{border-color:rgba(255,93,115,.35)}
.runtime.discharging .rt-main{color:var(--load)}
.runtime.charging{border-color:rgba(55,224,160,.35)}
.runtime.charging .rt-main{color:var(--soc)}
.runtime.idle .rt-main,.runtime.unknown .rt-main{color:var(--muted);font-size:22px}
.tile{background:var(--card);border:1px solid var(--card-brd);border-radius:18px;padding:18px 18px 16px;
  position:relative;overflow:hidden;backdrop-filter:blur(6px)}
.tile::before{content:"";position:absolute;inset:0 0 auto 0;height:3px;opacity:.9}
.tile.solar::before{background:linear-gradient(90deg,var(--solar),var(--solar2))}
.tile.batt::before{background:linear-gradient(90deg,var(--soc),#1fbf86)}
.tile.charge::before{background:linear-gradient(90deg,var(--charge),#5b48d6)}
.tile.energy::before{background:linear-gradient(90deg,#ffd54a,var(--solar))}
.tile-top{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px}
.tile-ico{font-size:16px}
.tile-label{text-transform:uppercase;letter-spacing:1.2px;font-weight:600}
.tile-value{font-size:40px;font-weight:760;margin:8px 0 2px;line-height:1;letter-spacing:-1px}
.tile.solar .tile-value{color:var(--solar)}
.tile.batt .tile-value{color:var(--soc)}
.tile.charge .tile-value{color:var(--charge)}
.tile.energy .tile-value{color:#ffd54a}
.unit{font-size:16px;font-weight:600;color:var(--muted);margin-left:5px}
.tile-foot{font-size:12.5px;color:var(--muted);margin-top:6px}
.card.gauge-card{padding:6px}                      /* ditto */
.gauge{width:100%;height:215px}

/* controls */
.controls{margin:22px 0 6px;display:flex;justify-content:center}
.ranges{display:inline-flex;gap:4px;background:var(--card);border:1px solid var(--card-brd);
  padding:5px;border-radius:14px}
.ranges button{background:transparent;border:0;color:var(--muted);font-weight:650;font-size:13px;
  padding:8px 15px;border-radius:10px;cursor:pointer;transition:.25s ease;letter-spacing:.5px}
.ranges button:hover{color:var(--text)}
.ranges button.active{background:linear-gradient(180deg,rgba(255,176,32,.22),rgba(255,122,0,.14));
  color:#fff;box-shadow:0 2px 12px rgba(255,140,0,.18)}
.stat .k{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.stat .v{font-size:20px;font-weight:720;margin-top:4px}
.stat .v small{font-size:12px;color:var(--muted);font-weight:600;margin-left:3px}

/* charts */
.charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
.card{background:var(--card);border:1px solid var(--card-brd);border-radius:18px;padding:16px 8px 8px 16px}
.card-title{font-size:14px;font-weight:700;color:var(--text);margin:2px 0 4px}
.card-title .hint{font-weight:500;color:var(--muted);font-size:12px}
.chart{width:100%;height:260px}

@media(max-width:900px){
  .topbar{flex-wrap:wrap;gap:12px;justify-content:center;grid-template-columns:1fr}
  .brand,.status{justify-self:center}
  .charts{grid-template-columns:1fr}
}
@media(max-width:520px){
  .tile-value{font-size:32px}
}

/* ============ FLOW VIEW — animated power-flow scene ============ */
#view-flow.active{display:flex}
.flow-scene{flex:1;display:flex;align-items:center;justify-content:center;
  min-height:calc(100dvh - var(--header-h,66px) - 24px);
  background:radial-gradient(700px 460px at 50% 8%, rgba(60,90,180,.10), transparent 70%);
  border:1px solid var(--card-brd);border-radius:14px;overflow:hidden;margin-top:2px}
#flowSvg{width:100%;height:100%;max-height:calc(100dvh - var(--header-h,66px) - 24px)}

/* connectors: straight orthogonal traces; JS toggles .live when carrying power */
.fl-wire{fill:none;stroke:rgba(180,195,235,.10);stroke-width:2;stroke-linejoin:miter;
  transition:stroke .5s ease}
.fl-wire.live{stroke:rgba(200,215,255,.22)}
.fl-junc{fill:#5a6a92}

/* sky — refined, static (no spin) */
.fl-sky{transition:opacity .8s ease}
.fl-sun-disc{fill:url(#flSunG)}
.fl-sun-halo{fill:#e6af50;opacity:.22}
.fl-rays line{stroke:#e0aa55;stroke-width:2;stroke-linecap:round;opacity:.7}
.fl-moon{fill:#c7d0ea}
.fl-star{fill:#c7d0ea;opacity:.7}

/* node modules — sharp rectangular tiles, monochrome line-art icons; colour
   comes only from data (battery fill, values, energy dots) = instrumentation */
.fl-tile{fill:rgba(13,19,40,.66);stroke:var(--card-brd);stroke-width:1}
.fl-div{stroke:var(--card-brd);stroke-width:1}
.fl-ico{fill:none;stroke:#aeb6d4;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.fl-batt-ico-cap{fill:#aeb6d4}
.fl-batt-ico-body{fill:rgba(255,255,255,.03);stroke:#aeb6d4;stroke-width:2}
#flBattFill{transition:height .7s cubic-bezier(.33,0,.15,1),y .7s cubic-bezier(.33,0,.15,1),fill .7s ease}
.fl-tile-lab{fill:var(--muted);font-size:11px;font-weight:650;letter-spacing:1px;
  text-transform:uppercase;dominant-baseline:middle}
.fl-tile-val{fill:var(--text);font-size:23px;font-weight:750;font-variant-numeric:tabular-nums;
  dominant-baseline:middle}
.fl-tile-val.g{fill:#37e0a0}.fl-tile-val.y{fill:#ffd23f}.fl-tile-val.r{fill:#ff8f6b}
.fl-tile-sub{fill:var(--muted);font-size:11.5px;font-weight:600;dominant-baseline:middle}
.fl-tile-sub.g{fill:#37e0a0}.fl-tile-sub.r{fill:#ff8f6b}

/* tiny inline voltage chips on the traces */
.fl-vchip rect{fill:rgba(11,16,34,.92);stroke:var(--card-brd);stroke-width:1}
.fl-vval{fill:#4aa3ff;font-size:13.5px;font-weight:700;font-variant-numeric:tabular-nums;
  dominant-baseline:middle}

/* cabin status strip (DC / AC / Sync) */
.fl-statbar{fill:rgba(13,19,40,.66);stroke:var(--card-brd);stroke-width:1}
.fl-stdot{fill:#6b7699}
.fl-stdot.on{fill:#37e0a0}
.fl-stdot.off{fill:#6b7699}
.fl-stlab{fill:var(--muted);font-size:11px;font-weight:700;letter-spacing:.4px;dominant-baseline:middle}
.fl-ststate{fill:var(--muted);font-size:11px;font-weight:700;dominant-baseline:middle}
.fl-ststate.on{fill:#37e0a0}
.fl-ststate.off{fill:#8b93b0}

/* battery-array temperature strip */
.fl-tt-lab{fill:var(--muted);font-size:10px;font-weight:700;letter-spacing:.6px;dominant-baseline:middle}
.fl-tt-val{fill:var(--text);font-size:16px;font-weight:750;font-variant-numeric:tabular-nums;dominant-baseline:middle}
.fl-tt-val.g{fill:#37e0a0}.fl-tt-val.y{fill:#ffd23f}.fl-tt-val.r{fill:#ff8f6b}
.fl-tt-spark{fill:none;stroke:#e0aa55;stroke-width:1.6;vector-effect:non-scaling-stroke;
  stroke-linejoin:round;stroke-linecap:round}

@media(max-width:899px){
  #view-flow.active,.flow-scene{min-height:calc(100dvh - var(--header-h,66px) - 14px)}
}
/* phones: the scene is wide, so a tall column just letterboxes it — let it take
   its natural aspect at the top instead (Flow is a desktop/tablet tab; the mobile
   tab bar is hidden, so this only matters on a direct /flow visit). */
@media(max-width:620px){
  #view-flow.active{display:block}
  .flow-scene{min-height:0;aspect-ratio:1000/560;margin-top:8px}
  #flowSvg{max-height:none}
}
