:root{
  --bg:#0d1110; --panel:#161c19; --panel2:#1f2826; --line:#2b3a33;
  --text:#eef3ef; --muted:#93a89b; --accent:#7ac74f; --warn:#ffb454; --bad:#ff6b6b;
}
*{box-sizing:border-box}
[hidden]{display:none !important}
/* LINKS ARE TOOLKIT GREEN, never browser blue. Default blue is the one colour the app uses
   nowhere else, so it reads as foreign against the dark panels — and browser-visited purple is
   worse. Lowest possible specificity, so anything setting its own colour (.lanbar a, buttons
   styled as links) still wins. */
a{color:var(--accent)}
a:visited{color:var(--accent)}
a:hover{text-decoration:underline}
/* Adaptive root font: never smaller than today's 16px, grows fluidly to 20px on big/4K screens so
   text stays readable on any display. EVERYTHING sized in rem/em scales from this one value. */
html{-webkit-text-size-adjust:100%;font-size:clamp(16px, 14px + 0.16vw, 20px)}
body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:var(--bg);color:var(--text);font-size:1rem;line-height:1.4;
  padding-bottom:env(safe-area-inset-bottom)}

.expbar{background:repeating-linear-gradient(45deg,#3a2a00,#3a2a00 12px,#332600 12px,#332600 24px);
  color:var(--warn);font-weight:700;font-size:.78rem;text-align:center;padding:.45rem .7rem;border-bottom:1px solid var(--warn)}

header{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap;
  /* reserve the top safe-area (notch / status bar) so the header — and everything below it — stays in the
     visible screen and never hides under a notch or the OS bar */
  padding:calc(.7rem + env(safe-area-inset-top)) max(1rem,env(safe-area-inset-right)) .7rem max(1rem,env(safe-area-inset-left));
  background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:5}
.brand{display:flex;align-items:center;gap:.5rem;font-weight:700}
.brand .logo{height:17px;width:auto}
.brand .brandsub{color:var(--accent)}
.picker{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
select{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.5rem .6rem;font-size:1rem;min-width:150px;max-width:60vw}
.conn{font-size:.75rem;padding:.2rem .55rem;border-radius:999px;font-weight:600}
.conn.on{background:rgba(122,199,79,.16);color:var(--accent)} .conn.off{background:rgba(255,107,107,.14);color:var(--bad)} .conn.stale{background:rgba(255,180,84,.16);color:var(--warn)}

.lanbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;background:var(--panel);border-bottom:1px solid var(--line);padding:.5rem 1rem;color:var(--muted);font-size:.85rem}
.lanbar a{color:var(--accent);font-family:monospace;text-decoration:none}

.tabs{display:flex;gap:.2rem;padding:.4rem 1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch;position:sticky;top:0;background:var(--bg);z-index:4}
.tab{background:transparent;color:var(--muted);border:none;border-bottom:2px solid transparent;padding:.6rem .9rem;font-size:.95rem;cursor:pointer;white-space:nowrap}
.tab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}

/* Edge-to-edge: the app uses the FULL window width on big screens (no 1000px cap), with responsive
   side padding. Text-heavy single-column tabs (Logs/Settings) keep a readable max-width so their rows
   don't stretch across an ultrawide; the Overview (map/camera/cards) and Remote stay full-bleed. */
main{padding:1rem clamp(1rem, 2vw, 2.5rem)}
#tab-logs, #tab-settings{max-width:72rem; margin-inline:auto}
.sech{margin:1.4rem 0 .4rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
/* Overview panels flow into columns on wide/ultrawide screens (minimize scroll), 1 column on mobile.
   Section headers span the full width; panels never split across a column break. */
.ovcols{column-width:340px;column-gap:1rem}
.ovcols>.sech{-webkit-column-span:all;column-span:all;margin-top:1rem}
.ovcols>.panel{-webkit-column-break-inside:avoid;break-inside:avoid}
.ovcols>.panel[hidden]{display:none}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:.9rem 1rem;margin-bottom:1rem}
.panel h2,.panel h3{margin:.1rem 0 .7rem;font-size:1rem}
.panel summary{cursor:pointer;color:var(--muted);font-size:.95rem}
/* Advanced Data tab — collapsible items (Mowing history, Diagnostics, Logs) */
.advitem{margin-bottom:.6rem}
.advitem>summary{font-size:1rem;font-weight:600;color:var(--text);padding:.15rem 0}
.advitem[open]>summary{margin-bottom:.6rem;border-bottom:1px solid var(--line);padding-bottom:.5rem}
.logctls{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;margin:.2rem 0 .7rem}
.rowbetween{display:flex;align-items:center;justify-content:space-between;gap:.5rem}
.sub{color:var(--muted);font-size:.85rem;margin:.2rem 0 .7rem}

/* compact cards so the Overview doesn't scroll forever: tighter padding/fonts. Flexbox (not grid)
   so the whole ROW of cards CENTERS — incl. a partial last row. (Grid's auto-fill made phantom
   full-width tracks, so justify-content:center had no effect and the row sat left-justified.) */
.cards{display:flex;flex-wrap:wrap;justify-content:center;gap:.55rem}
.empty{flex:1 1 100%;color:var(--muted);text-align:center;padding:1.5rem}
.card{flex:0 1 160px;background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:.5rem .6rem}
.card h3{margin:0 0 .2rem;font-size:.68rem;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);display:flex;gap:.3rem;align-items:center}
.card .val{font-size:1.2rem;font-weight:700;line-height:1.1}
.card .sub{margin:.15rem 0 0;font-size:.72rem;color:var(--muted)}
.card.good .val{color:var(--accent)} .card.warn .val{color:var(--warn)} .card.bad .val{color:var(--bad)}
.bar{height:7px;border-radius:6px;background:var(--panel2);margin-top:.5rem;overflow:hidden}.bar>span{display:block;height:100%;background:var(--accent)}

.btns{display:flex;flex-wrap:wrap;gap:.5rem}
.btn{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:10px;padding:.6rem 1rem;font-size:.95rem;cursor:pointer;min-height:42px}
.btn:hover{border-color:var(--accent)} .btn:active{transform:translateY(1px)}
.btn.ghost{background:transparent} .btn.small{padding:.35rem .6rem;min-height:0;font-size:.82rem}
.btn.block{width:100%} .btn.danger{border-color:var(--bad);color:var(--bad)} .btn.danger:hover{background:rgba(255,107,107,.12)}
.segbtns{display:inline-flex;border:1px solid var(--line);border-radius:10px;overflow:hidden}
.segbtns .segbtn{background:var(--panel2);color:var(--muted);border:0;border-left:1px solid var(--line);padding:.5rem .9rem;font-size:.9rem;cursor:pointer;min-height:38px}
.segbtns .segbtn:first-child{border-left:0}
.segbtns .segbtn:hover{color:var(--text)}
.segbtns .segbtn.on{background:var(--accent);color:#0b140b;font-weight:600}

.kvwrap{display:block;max-width:60rem}   /* keep key/value rows readable even in a full-width panel */
/* "Download all to this PC" is desktop-only — hide it on touch/mobile (a downloaded .zip is unusable there).
   CSS (primary pointer is coarse = phone/tablet) so it ALWAYS shows on a desktop, independent of JS timing. */
@media (pointer:coarse){ #backupmaps{display:none !important} }
.kv{display:flex;justify-content:space-between;gap:1rem;padding:.35rem 0;border-bottom:1px solid var(--line)}
.kv:last-child{border-bottom:none}
.kv .k{color:var(--muted);display:flex;gap:.35rem;align-items:center}.kv .v{font-weight:600;text-align:right;word-break:break-word}
/* mow history list (recent + full-history modal) */
.mowtot1{display:flex;flex-wrap:wrap;gap:.4rem 1.4rem;align-items:baseline}   /* 3 lifetime totals on one line (wraps only on very narrow screens) */
.mowtot1 .mtv{font-weight:700}
.mowtot1 .mtk{color:var(--muted);font-size:.9rem}
.mowlisthead{display:flex;justify-content:space-between;align-items:center;gap:1rem}
.mowlist{display:block;max-width:60rem}
.mrow{padding:.5rem 0;border-bottom:1px solid var(--line)}
.mrow:last-child{border-bottom:none}
.mrow-top{display:flex;justify-content:space-between;align-items:center;gap:1rem}
.mrow-date{font-weight:600}
.mrow-mid{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-top:.2rem;color:var(--muted)}
.mmeta{white-space:nowrap;text-align:right;flex:0 0 auto}
.mzones{word-break:break-word}
.mstatus{font-size:.78rem;font-weight:700;padding:.1rem .5rem;border-radius:1rem;white-space:nowrap}
.ms-ok{color:#0a7c2f;background:rgba(16,160,64,.14)}
.ms-int{color:#b06a00;background:rgba(224,160,0,.16)}
/* zone disclosure: stack names, custom +/- marker (no comma overflow) */
.mzdet{display:inline-block}
.mzdet>summary{cursor:pointer;list-style:none;user-select:none}
.mzdet>summary::-webkit-details-marker{display:none}
.mzdet>summary::before{content:"+ ";font-weight:700;color:var(--accent,#2f81f7)}
.mzdet[open]>summary::before{content:"– "}
.mzstack{margin:.25rem 0 .1rem .9rem}
.mzitem{padding:.1rem 0}
/* full-history modal */
.histbox{max-width:44rem;width:min(44rem,92vw);max-height:86vh;display:flex;flex-direction:column}
.histmodalhead{display:flex;justify-content:space-between;align-items:center;gap:1rem;margin-bottom:.5rem}
#histall{overflow-y:auto;flex:1 1 auto}
.histfoot{display:flex;justify-content:space-between;align-items:center;gap:1rem;margin-top:.6rem}
.kvgroup{margin:.5rem 0;border:1px solid var(--line);border-radius:8px;padding:.3rem .7rem}
.kvgroup-h{color:var(--accent);font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;margin:.3rem 0;font-weight:600}
.q-good{color:var(--accent);font-weight:700}.q-warn{color:var(--warn);font-weight:700}.q-bad{color:var(--bad);font-weight:700}
.camwrap{position:relative;background:#000;border-radius:12px;overflow:hidden;aspect-ratio:16/9;margin:.5rem 0;display:flex;align-items:center;justify-content:center}
.camwrap img,.camwrap video{width:100%;height:100%;object-fit:contain;background:#000}
.camstatus{position:absolute;top:.5rem;left:.6rem;background:rgba(0,0,0,.6);color:var(--accent);padding:.2rem .6rem;border-radius:6px;font-size:.8rem;font-weight:600}
.camfs{position:absolute;right:.5rem;bottom:.5rem;z-index:4;background:rgba(0,0,0,.55)}
/* height uses dvh (dynamic viewport height) so mobile browser chrome (URL bar) doesn't get
   counted — 100vh would extend behind the bar and clip the camera. vh first as a fallback. */
.camwrap:fullscreen{width:100vw;height:100vh;height:100dvh;aspect-ratio:auto}
.camwrap:fullscreen img{width:100vw;height:100vh;height:100dvh;object-fit:contain}
.camwrap.pseudofs{position:fixed;top:0;left:0;z-index:100;width:100vw;height:100vh;height:100dvh;max-width:100vw;aspect-ratio:auto;border-radius:0}
.camwrap.pseudofs img{width:100vw;height:100vh;height:100dvh;object-fit:contain}
/* rotate-to-fullscreen (landscape + camera on): cover the viewport, keep joystick on top */
.camwrap.fs{position:fixed;top:0;left:0;z-index:1000;width:100vw;height:100vh;height:100dvh;max-width:100vw;aspect-ratio:auto;border-radius:0;margin:0}
.camwrap.fs img{width:100vw;height:100vh;height:100dvh;object-fit:contain}
/* LED quick-toggle: colored glow when on, gray frame when off (upper-left, no scrolling) */
.camled{position:absolute;top:.5rem;left:.6rem;z-index:7;width:34px;height:34px;border-radius:50%;
  border:2px solid rgba(255,255,255,.5);background:rgba(0,0,0,.45);font-size:1rem;line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;padding:0;filter:grayscale(1) opacity(.6);transition:all .15s}
.camled.on{border-color:#ffd33d;background:rgba(255,211,61,.2);box-shadow:0 0 12px 2px rgba(255,211,61,.7);filter:none}
#camwrap .camstatus{left:3.2rem}
/* the popup Camera view shows its 💡 only in fullscreen/landscape — shift the status pill right of it
   there too (both sat at the same top-left spot, so the icon overlapped the status text) */
.cvwrap.fs .camstatus,.cvwrap:fullscreen .camstatus{left:3.2rem}
/* live position mini-map (upper-right, above the controls) */
.minimap{position:absolute;top:.5rem;right:.5rem;z-index:7;width:34%;max-width:230px;min-width:120px;aspect-ratio:4/3;
  border-radius:8px;overflow:hidden;border:2px solid rgba(255,255,255,.6);box-shadow:0 2px 10px rgba(0,0,0,.55);
  pointer-events:auto;background:#0d1117}
.minimap.hidden{display:none}
.minirobot{filter:drop-shadow(0 0 4px rgba(0,0,0,.85)) drop-shadow(0 0 6px rgba(0,229,255,.5))}
.minimap .leaflet-control-container{display:none}
/* mini-map zoom (+/-) overlaid on the map (top-left) + a drag-to-resize grip in the lower-left
   corner. They live INSIDE the map box so they track it as it's resized. Shared by the Remote
   camera and the Overview popup; visible whenever the map itself is shown. */
.minimap .mmctrls{position:absolute;top:.3rem;left:.3rem;right:auto;bottom:auto;z-index:1000;
  display:flex;flex-direction:column;gap:.3rem}
.mmbtn{width:30px;height:30px;border-radius:8px;border:1px solid #2b3340;background:rgba(0,0,0,.6);
  color:#e6edf3;font-size:1.15rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;
  user-select:none;touch-action:manipulation}
.mmbtn:hover{background:rgba(0,0,0,.85)}
.minimap .mmgrip{position:absolute;left:2px;bottom:2px;width:16px;height:16px;z-index:1000;cursor:nesw-resize;
  border-radius:3px;touch-action:none;pointer-events:auto;
  background:linear-gradient(45deg,transparent 0 42%,rgba(255,255,255,.75) 42% 52%,transparent 52% 60%,rgba(255,255,255,.75) 60% 70%,transparent 70%)}
/* Overview popup camera: the light only shows in fullscreen / landscape (the position map carries
   its own controls and is itself hidden unless fullscreen). */
.cvwrap .camled{display:none!important}
.cvwrap.fs .camled,.cvwrap:fullscreen .camled{display:flex!important}
@media (max-width:520px){.minimap{width:42%;top:.4rem;right:.4rem}.camled{width:30px;height:30px}#camwrap .camstatus,.cvwrap.fs .camstatus,.cvwrap:fullscreen .camstatus{left:2.9rem}}
/* ---- Remote tab fit-on-one-screen: DESKTOP/MOUSE ONLY (pointer:fine). On a phone/tablet (touch) the
   camera goes TRUE fullscreen on landscape via the orientation handler — width is irrelevant there — so
   none of this height-capping / 2-column layout applies to touch (px is the wrong signal for that). The
   :not(.fs):not(.pseudofs) guard is critical: it stops the cap from shrinking a fullscreen camera. ---- */
@media (pointer:fine){ #tab-remote .camwrap:not(.fs):not(.pseudofs){max-height:58dvh} }
@media (pointer:fine) and (max-height:760px){ #tab-remote .camwrap:not(.fs):not(.pseudofs){max-height:50dvh} }
@media (pointer:fine) and (max-height:600px){
  #tab-remote .camwrap:not(.fs):not(.pseudofs){max-height:42dvh}
  #tab-remote .prow{margin:.25rem 0}
  #tab-remote .panel{padding:.6rem .8rem}
  #tab-remote .rchint{display:none}
}
@media (pointer:fine) and (min-width:860px){
  #tab-remote{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,380px);gap:1rem;align-items:start}
  #tab-remote > section{margin-bottom:0}
}
.camwrap.fs .joyoverlay{opacity:.92}
/* In camera FULLSCREEN nothing else may overlay the view — hide the floating control bar (z1200) and the
   add-to-home banner so the live feed + joystick own the whole screen (this was the bottom 'bullshit'). */
body.camfs-lock{overflow:hidden}
body.camfs-lock .floatbar, body.camfs-lock .a2hs{display:none !important}
/* compact telemetry top-bar (centered pill) — on the map overlay + fullscreen cameras */
.mapwrap{position:relative}
.telbar{position:absolute;top:.4rem;left:50%;transform:translateX(-50%);z-index:6;
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:.15rem .7rem;max-width:94%;line-height:1;
  background:rgba(0,0,0,.62);border:1px solid rgba(255,255,255,.18);border-radius:16px;
  padding:.26rem .75rem;font-size:.8rem;color:#e6edf3;box-shadow:0 2px 8px rgba(0,0,0,.45);pointer-events:none}
.telbar:empty{display:none}
/* each group (sensors / status+progress) stays on its own line; status+progress drop to row 2 on phones */
.telbar .tb-row{display:inline-flex;align-items:center;gap:.7rem;white-space:nowrap}
.telbar .tb-st{font-weight:600}
.telbar .tb-prog{font-weight:600}
.telbar .tb-batt{display:inline-flex;align-items:center;gap:.3rem;font-variant-numeric:tabular-nums}
.telbar .tb-batt.good{color:#3fb950}.telbar .tb-batt.warn{color:#d29922}.telbar .tb-batt.bad{color:#f85149}
.telbar .tb-bw{position:relative;width:13px;height:20px;border:1.6px solid currentColor;border-radius:3px;display:inline-block;overflow:hidden}
.telbar .tb-bw::after{content:"";position:absolute;top:-3.4px;left:50%;transform:translateX(-50%);width:6px;height:2px;background:currentColor;border-radius:1px}
.telbar .tb-bf{position:absolute;left:0;bottom:0;width:100%;background:currentColor}
/* ⚡ charging, on the battery chip (v1.45.1). Always the charging YELLOW, never the battery's
   good/warn/bad color: at 12% the chip is red, and "red lightning" reads as a fault when it is
   actually the good news that the mower is on the charger. */
.telbar .tb-chg{color:#f2c744;margin-left:.1rem;line-height:1;font-size:.85em}
.telbar .tb-ico{display:inline-flex;align-items:center}
.telbar .tb-cell{font-weight:700;font-size:.74rem;letter-spacing:.02em}
.telbar .tb-rtk{display:inline-flex;align-items:center;gap:.2rem;font-variant-numeric:tabular-nums}
.telbar .tb-rtk .tb-acc{color:#e6edf3}   /* only the pin is color-coded; keep the number readable */
.telbar .tb-green{color:#3fb950}.telbar .tb-orange{color:#d29922}.telbar .tb-red{color:#f85149}.telbar .tb-grey{color:#8b949e}
/* camera bars show only in fullscreen / landscape (like the light + map controls) */
/* The camera status bar shows in the Remote camera at ALL times (not just fullscreen) — you need the
   mower's status while driving, and losing it on exiting fullscreen was pure friction. It stays
   hidden until the feed is actually live (.camlive) so it can't float over a dead/black frame.
   The Overview popup camera keeps the original fullscreen-only behavior (it's a peek, not a cockpit). */
.camtelbar{display:none}
.camwrap.camlive .camtelbar,
.camwrap.fs .camtelbar,.camwrap:fullscreen .camtelbar,.camwrap.pseudofs .camtelbar,
.cvwrap.fs .camtelbar,.cvwrap:fullscreen .camtelbar{display:flex}
.camexitfs{position:absolute;top:.5rem;right:.5rem;z-index:1001;background:rgba(0,0,0,.55)}
.camwrap.fs .camexitfs{display:block}
/* Remote camera "go fullscreen" button — you could only reach fullscreen by rotating to landscape.
   BOTTOM-LEFT on purpose: the joystick owns bottom-right (.joyoverlay), the mini-map top-right, and the
   status bar top-center. Only while the feed is live and NOT already fullscreen (Exit covers that). */
.camfsbtn{position:absolute;bottom:.5rem;left:.5rem;z-index:8;display:none;
  background:rgba(0,0,0,.55);padding:.25rem .55rem;line-height:1;font-size:1rem}
.camwrap.camlive .camfsbtn{display:block}
.camwrap.fs .camfsbtn,.camwrap:fullscreen .camfsbtn,.camwrap.pseudofs .camfsbtn{display:none}
/* camera-link (Auto/WiFi/4G) chip inside the camera — only shown in fullscreen, where the settings row is
   hidden. Bottom-LEFT corner: clear of the telemetry bar (top-center), LED/status (top-left),
   Exit (top-right) and the Fullscreen button (bottom-right). */
.camsrc-fs{position:absolute;bottom:.5rem;left:.5rem;z-index:1001;display:none}
.camwrap.fs .camsrc-fs,.camwrap:fullscreen .camsrc-fs,.camwrap.pseudofs .camsrc-fs{display:inline-flex}
.camsrc-fs .segbtn{background:rgba(0,0,0,.55);color:#cfe8c4;padding:.3rem .7rem;min-height:32px;font-size:.8rem}
.camsrc-fs .segbtn.on{background:var(--accent);color:#0b140b}
/* joystick overlaid on the camera, lower-right, semi-transparent */
.joyoverlay{position:absolute;right:14px;bottom:14px;z-index:3;opacity:.5;transition:opacity .15s}
.joyoverlay:hover,.joyoverlay.active{opacity:.92}
.joy{--sz:180px;position:relative;width:var(--sz);height:var(--sz);border-radius:50%;
  background:radial-gradient(circle,rgba(31,40,38,.85),rgba(13,17,16,.7));
  border:2px solid rgba(122,199,79,.6);touch-action:none;cursor:grab}
.joy.disabled{opacity:.4;cursor:not-allowed}
.joy::after{content:"";position:absolute;inset:0;border-radius:50%;
  background:linear-gradient(rgba(255,255,255,.25),rgba(255,255,255,.25)) center/2px 100% no-repeat,linear-gradient(rgba(255,255,255,.25),rgba(255,255,255,.25)) center/100% 2px no-repeat}
.knob{position:absolute;left:50%;top:50%;width:calc(var(--sz)*0.36);height:calc(var(--sz)*0.36);border-radius:50%;
  background:var(--accent);transform:translate(-50%,-50%);box-shadow:0 4px 14px rgba(0,0,0,.6)}
.joy.disabled .knob{background:var(--muted)}
input[type=range]{flex:1;accent-color:var(--accent)}
.rngval{min-width:64px;text-align:right;font-weight:600;color:var(--accent)}
/* v1.39 numeric STEPPER — ▼ [value] ▲ with a typable box. Replaces the range slider on the limits
   that now span 0.04 m to 100 m: one pixel of a linear slider across that range is ~0.4 m, so
   picking 0.10 vs 0.50 by dragging was impossible, worst of all on a phone. Buttons are 44px
   touch targets (hold to repeat, accelerating); the box takes an exact typed value. */
.stepper{display:inline-flex;align-items:center;gap:.3rem}
/* both arrows sit TOGETHER to the right of the number (never split around it), joined into one
   pill so they read as a single control */
.steparrows{display:inline-flex;align-items:center}
.steparrows .stepbtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}
.steparrows .stepbtn:last-child{border-top-left-radius:0;border-bottom-left-radius:0;border-left:none}
.stepbtn{min-width:44px;min-height:44px;padding:.3rem .5rem;font-size:1rem;line-height:1;font-weight:700;
  background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:10px;
  cursor:pointer;touch-action:manipulation;-webkit-user-select:none;user-select:none}
.stepbtn:active{background:var(--accent);color:#0d1110;border-color:var(--accent)}
.stepval{width:6rem;min-height:44px;text-align:right;font-weight:600;color:var(--accent);
  background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:.4rem .5rem;font-size:1rem}
/* hide the browser's own tiny spinners — ours are the ▼▲ buttons, which are thumb-sized */
.stepval::-webkit-outer-spin-button,.stepval::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.stepval{-moz-appearance:textfield;appearance:textfield}
.stepunit{min-width:3.2rem;color:var(--muted);font-size:.9rem}

.adv{display:flex;align-items:center;gap:.5rem;color:var(--warn);margin:.3rem 0 .8rem;font-size:.9rem}
.toggle{display:inline-flex;align-items:center;gap:.6rem;cursor:pointer;font-size:.95rem;user-select:none}
.toggle input{position:absolute;opacity:0;width:0;height:0}
.toggle .slider{position:relative;width:46px;height:26px;background:var(--panel2);border:1px solid var(--line);border-radius:999px;transition:.2s;flex:0 0 auto}
.toggle .slider::before{content:"";position:absolute;left:3px;top:2px;width:20px;height:20px;border-radius:50%;background:var(--muted);transition:.2s}
.toggle input:checked + .slider{background:rgba(122,199,79,.3);border-color:var(--accent)}
.toggle input:checked + .slider::before{transform:translateX(20px);background:var(--accent)}
.prow{display:flex;align-items:center;gap:.8rem;margin:.5rem 0;flex-wrap:wrap}
.prow label{flex:1 1 180px;color:var(--muted);font-size:.9rem;display:flex;gap:.35rem;align-items:center}
.prow input{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.55rem .6rem;width:130px;font-size:1rem}
.paramactions{margin-top:.9rem;display:flex;align-items:center;gap:.7rem;flex-wrap:wrap}
.msg{color:var(--accent);font-size:.85rem}

.alerts{display:flex;flex-direction:column;gap:.5rem;margin-bottom:.8rem}
.alert{background:rgba(255,180,84,.14);border:1px solid var(--warn);color:var(--warn);padding:.6rem .9rem;border-radius:10px;font-weight:600;font-size:.9rem}
.alert.crit{background:rgba(255,107,107,.14);border-color:var(--bad);color:var(--bad)}
.loglist{font-family:monospace;font-size:.8rem;max-height:50vh;overflow:auto}
.logline{padding:.3rem 0;border-bottom:1px solid var(--line)}.logline .muted{color:var(--muted)}

.help{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--panel2);color:var(--muted);font-size:.7rem;font-weight:700;cursor:help;border:1px solid var(--line);flex:0 0 auto}
.help:hover{background:var(--accent);color:#04140d}
.tooltip{position:fixed;max-width:280px;background:#000;color:#fff;padding:.55rem .7rem;border-radius:8px;font-size:.82rem;line-height:1.35;z-index:3500;box-shadow:0 6px 24px rgba(0,0,0,.5);border:1px solid var(--line);pointer-events:none}   /* above the modal (z1300) so ? help is readable inside modals */

.modal{position:fixed;inset:0;background:rgba(6,9,8,.86);display:flex;align-items:flex-start;justify-content:center;z-index:1300;padding:1rem;overflow:auto}   /* top-aligned so a tall modal always opens at its TOP (never clipped above the viewport); above the floating control bar (z1200) */
.modalbox{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:1.4rem;width:100%;max-width:min(560px, 94vw);max-height:92vh;overflow:auto;margin:auto 0}
.modalbox h2{margin:0 0 .6rem}
.modalbox input[type=email],.modalbox input[type=password],.modalbox input[type=text]{width:100%;background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.7rem;font-size:1rem;margin:.4rem 0}
.gatelabel{display:block;font-size:.82rem;color:var(--muted);margin:.3rem 0}
.gatelabel .sel.block,select.sel.block{width:100%;background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.6rem;font-size:1rem;margin:.25rem 0;max-width:none}
.gatesplit{text-align:center;color:var(--muted);font-size:.8rem;margin:.6rem 0}
.gatesoon{text-align:center;color:var(--muted);font-size:.92rem;border:1px dashed var(--line);border-radius:10px;padding:.7rem;background:var(--panel2)}
.gatesoon .sub{font-size:.78rem;margin-top:.2rem}
.gatephone{margin-top:.7rem;border:1px solid var(--line);border-radius:10px;background:var(--panel2)}
.gatephone>summary{cursor:pointer;padding:.6rem .7rem;font-size:.9rem;color:var(--text);list-style:none;user-select:none}
.gatephone>summary::-webkit-details-marker{display:none}
.gatephone[open]>summary{border-bottom:1px solid var(--line)}
.gatephone #gatephonebody{padding:.7rem}
.gatephone #qrbox{text-align:center;margin:.4rem 0}
#goauthbox{margin-top:.3rem;border-top:1px solid var(--line);padding-top:.5rem}
#goauthbox code{background:var(--panel2);padding:0 .2rem;border-radius:4px}
/* v1.37 unified Google sign-in (gate) */
.gbtn{background:#fff;color:#1f1f1f;border:1px solid var(--line);font-weight:600}
.gbtn:hover{background:#f1f3f4}
.gwait{display:flex;align-items:center;gap:.55rem;color:var(--muted);font-size:.9rem;
  border:1px dashed var(--line);border-radius:10px;padding:.7rem;margin:.5rem 0;background:var(--panel2)}
.gspin{width:14px;height:14px;flex:0 0 14px;border:2px solid var(--line);border-top-color:var(--accent);
  border-radius:50%;animation:gspin 1s linear infinite}
@keyframes gspin{to{transform:rotate(360deg)}}
.gok{color:var(--ok,#2ea043);border-style:solid}
.gok span{font-weight:700}
.gfallback{margin:.4rem 0}
.gfallback summary{color:var(--muted);font-size:.8rem;cursor:pointer}
.gfallback input{width:100%;box-sizing:border-box;margin:.4rem 0}
a.btn.block.gauthlink{display:block;text-align:center;text-decoration:none;margin:.2rem 0 .5rem}
.modalbtns{display:flex;gap:.6rem;justify-content:flex-end;margin-top:1rem;flex-wrap:wrap}
.zopt{display:block;padding:.5rem 0;font-size:.95rem}
.zlist{max-height:40vh;overflow:auto;border:1px solid var(--line);border-radius:8px;padding:.4rem .6rem;margin:.3rem 0}
.zitem{display:block;padding:.4rem 0}
#qrbox{display:flex;justify-content:center;align-items:center;background:#fff;padding:.6rem;border-radius:10px;margin:.5rem auto;width:max-content;max-width:100%}
#qrbox svg,#qrbox img.qr{display:block;width:220px;height:220px;margin:0 auto;image-rendering:pixelated}
.lanurl{text-align:center;word-break:break-all}
/* add-to-home banner */
.a2hs{position:fixed;left:0;right:0;bottom:0;z-index:200;display:flex;align-items:center;
  justify-content:space-between;gap:.6rem;padding:.6rem .9rem;background:var(--accent);
  color:#06120a;font-weight:600;box-shadow:0 -4px 14px rgba(0,0,0,.45)}
.a2hs .a2hs-btns{display:flex;gap:.4rem;flex:0 0 auto}
.a2hs .btn.ghost{color:#06120a;border-color:rgba(0,0,0,.3)}
.lanurl{font-family:monospace;color:var(--accent);text-align:center;word-break:break-all;margin-top:.6rem}

/* USER RULE (2026-07-15): transient feedback ALWAYS sits in the MIDDLE of the screen, never at the
   bottom — the toast used to render bottom-center at z70, directly UNDER the floating control bar
   (bottom-center, z1200), so every message it showed was invisible and button presses read as "nothing
   happened". Centered + z4500 puts it above the control bar (1200), modal (1300), tooltip (3500) and
   the game-stream overlay (4000). pointer-events:none so a centered message can never block a control.
   (flashOK success sits at 42%, the toast at 52% — both mid-screen, never covering each other.) */
.toast{position:fixed;left:50%;top:52%;transform:translate(-50%,-50%) scale(.96);background:#000;color:#fff;padding:.8rem 1.2rem;border-radius:10px;border:1px solid var(--accent);font-size:.95rem;opacity:0;transition:.25s;z-index:4500;pointer-events:none;max-width:90vw;text-align:center;box-shadow:0 8px 30px rgba(0,0,0,.55)}
.toast.show{opacity:1;transform:translate(-50%,-50%) scale(1)}
/* center-screen success confirmation ("✓ Saved Successfully") — non-blocking, ~2s, above everything */
.flashok{position:fixed;left:50%;top:42%;transform:translate(-50%,-50%) scale(.94);z-index:4500;pointer-events:none;   /* above the game-stream overlay (z4000) — a confirmation must NEVER be covered by anything */
  display:flex;align-items:center;gap:.6rem;background:rgba(16,22,18,.97);color:var(--text);
  border:1px solid var(--accent);border-radius:16px;padding:1rem 1.5rem;font-size:1.15rem;font-weight:700;
  box-shadow:0 14px 50px rgba(0,0,0,.6);opacity:0;transition:opacity .18s ease,transform .18s ease;
  max-width:90vw;text-align:center}
.flashok.show{opacity:1;transform:translate(-50%,-50%) scale(1)}
.flashok .fok-ic{display:inline-flex;align-items:center;justify-content:center;width:1.7rem;height:1.7rem;
  border-radius:50%;background:var(--accent);color:#06120a;font-size:1.05rem;font-weight:900;flex:0 0 auto}

@media(max-width:600px){
  main{padding:.7rem}
  .cards{gap:.45rem}
  .card{flex-basis:132px}
  header{padding:.6rem .8rem}
  select{max-width:48vw}
  .card .val{font-size:1.1rem}
  .btn{flex:1 1 auto;text-align:center}
  #leafletmap{height:300px}
  /* when the cut panel is open on a phone, shrink the map so the map + the top
     Cut-angle row are visible together */
  #mapview.cutopen #leafletmap{height:45vh}
}

/* ---- interactive map card ---- */
.maptools{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;margin:.2rem 0 .6rem}
.maptools .sel{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.4rem .5rem;min-height:38px}
.maptools .inp{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.4rem .6rem;min-height:38px;flex:1 1 220px;min-width:160px}
.maptools .btn{min-height:38px;padding:.4rem .7rem}
#leafletmap{height:420px;width:100%;border-radius:12px;border:1px solid var(--line);background:#0d1117;z-index:0}
.leaflet-container{background:#0d1117;font:inherit}
.mapmark{display:flex;align-items:center;justify-content:center;font-size:20px;line-height:1;text-shadow:0 0 3px #000,0 0 3px #000}
.mapmark.robot span{color:#57d98a;font-size:22px}
.mapmark.dock span{font-size:18px}
.mapmark.rtk span{font-size:16px}
/* image markers (real Lymow mower / dock / RTK icons): NO white background, NO circle — just a thin
   shape-following outline glow so the dark silhouettes stay legible on satellite + grass. The mower's
   inner img rotates to heading. */
.mapbadge{display:flex;align-items:center;justify-content:center;background:transparent;
  filter:drop-shadow(0 0 1.5px rgba(255,255,255,.95)) drop-shadow(0 0 2px rgba(0,0,0,.6))}
.mapbadge img{display:block}
#leafletmap.imggrab{cursor:grab}
#leafletmap.imggrab:active{cursor:grabbing}
#leafletmap.imggrab .leaflet-interactive{pointer-events:none}
/* no browser focus rectangle around a clicked zone polygon — the color change is the cue */
#leafletmap .leaflet-interactive:focus,#leafletmap svg:focus,#leafletmap path:focus{outline:none}
/* inline cutting-parameters panel (keeps the map visible while editing) */
.cutparams-btn{margin-top:0}
.mapbtnrow{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.6rem}
/* scheduled-mows list under the map */
.schedlist{margin-top:.6rem}
.schedhead{font-size:.85rem;color:var(--muted);margin:.3rem 0}
.schedrow{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap;
  background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:9px;padding:.45rem .6rem;margin-bottom:.35rem}
.schedrow.off{opacity:.55}
.schedmain{display:flex;flex-direction:column;min-width:0}
.schedmeta{color:var(--muted);font-size:.78rem}
.schederr{display:block;color:#f0883e;font-size:.74rem;margin-top:.15rem}
.timesels{display:inline-flex;align-items:center;gap:.35rem}
.timesel{min-width:3.6rem;text-align:center;padding:.5rem .4rem;font-size:1rem;font-variant-numeric:tabular-nums}
.timesel#sc_ap{min-width:4rem}
.timecolon{font-weight:700;opacity:.7}
@media(max-width:560px){ .timesel{min-width:4.2rem;padding:.6rem .4rem;font-size:1.05rem} }
.schedbtns{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}
.schedbtns .danger{color:#f85149}
/* schedule modal bits */
.fieldblock{margin:.4rem 0}
.sublabel{font-size:.85rem;color:var(--muted);margin:.1rem 0 .3rem}
.dowpick{display:flex;flex-wrap:wrap;gap:.3rem}
.dowchip{display:inline-flex;align-items:center;gap:.25rem;background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:6px;padding:.2rem .45rem;font-size:.8rem;white-space:nowrap;flex:0 0 auto}
.datewrap{display:inline-flex;align-items:center;gap:.35rem}
.dateicon{background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:8px;padding:.25rem .5rem;cursor:pointer;font-size:1.05rem;line-height:1;color:#fff}   /* white so the calendar glyph is visible on the dark field */
.datewrap input[type=date]{color-scheme:dark}
/* the native date field draws its OWN calendar picker icon on the right — hide it; our white 🗓 button to the LEFT is the single picker trigger */
.datewrap input[type=date]::-webkit-calendar-picker-indicator{display:none;-webkit-appearance:none;opacity:0}
.datewrap input[type=date]::-webkit-inner-spin-button{display:none}
.zpick{display:flex;flex-direction:column;gap:.2rem;max-height:30vh;overflow:auto;margin:.2rem 0 .4rem;padding:.3rem;border:1px solid var(--line,#26302a);border-radius:8px}
.zpick label{display:flex;align-items:center;gap:.4rem;font-size:.85rem}
.zmapgrp{margin:.15rem 0}
.zmaphdr{font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:.15rem 0}
.zmapgrp>summary{cursor:pointer;font-size:.82rem;font-weight:600;padding:.15rem 0}
.zmapzones{display:flex;flex-direction:column;gap:.2rem;padding:.1rem 0 .2rem .6rem}
.sc_thumbwrap{margin:.5rem 0;text-align:center}
.sc_thumb{display:inline-block;width:240px;max-width:100%;height:150px;background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:10px;overflow:hidden}
.sc_thumb .cppvsvg,.sc_thumb svg{width:100%;height:100%}
.sc_thumb .thumbempty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:.8rem}
.cppvcap{font-size:.78rem;color:var(--muted);margin-top:.25rem}
.sc_params{margin:.5rem 0;border:1px solid var(--line,#26302a);border-radius:10px;padding:.3rem .6rem;background:var(--panel2,#141a16)}
.sc_params>summary{cursor:pointer;font-weight:600;padding:.3rem 0;list-style:revert}
.sc_pfields{margin-top:.3rem}
.sc_pfields.disabled{opacity:.45;pointer-events:none}   /* master toggle off = using saved settings */
/* v1.40: "different angle per pass" grays out when there is no specified angle to rotate FROM (an
   Optimized regular angle with no cross-cut pass). The ? hover stays reachable so the reason is visible. */
.mp_offrow.disabled{opacity:.5}
.mp_offrow.disabled input{cursor:not-allowed}
/* v1.40: each multi-pass pass is now a small card (height + mowing speed + blade speed) so three
   controls per pass stay visually grouped instead of running together into one long list. */
.mppass{border:1px solid var(--line);border-radius:10px;padding:.4rem .6rem;margin:.45rem 0;background:var(--panel)}
.mppasst{font-weight:700;color:var(--accent);font-size:.85rem;margin-bottom:.15rem}
.mppass .prow{margin:.3rem 0}
.mppass select{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.35rem .5rem;min-height:38px}
/* upcoming scheduled-mow chips on the calendar (dashed = future/planned, vs solid = completed) */
.calchip.sched{background:transparent;color:var(--text,#e8f0e8);border:1px dashed var(--accent,#7ac74f)}
.calchip.sched.off{opacity:.5;border-style:dotted}
.calchip.sched.run{background:rgba(122,199,79,.18);border-style:solid;font-weight:600}
.calchip.native{background:transparent;color:var(--text,#e8f0e8);border:1px dotted #6ea8ff}   /* official-app schedule (read-only) */
.calchip.conflict,.calrow.conflict{border-color:#f0883e !important;color:#f0883e}
.calrow.native{opacity:.9}
.calconflict{background:rgba(240,136,62,.12);border:1px solid #f0883e;border-radius:8px;padding:.4rem .6rem;margin:.2rem 0 .5rem;font-size:.82rem;color:#f0883e}
.calrow.sched{border-left-color:var(--accent,#7ac74f);border-left-style:dashed}
.calrow.sched.running{border-left-style:solid;background:rgba(122,199,79,.1)}
.schedrow.running{border-color:var(--accent,#7ac74f);box-shadow:inset 0 0 0 1px var(--accent,#7ac74f)}
.cutpanel{margin-top:.6rem;padding:.7rem;border:1px solid var(--line);border-radius:10px;background:var(--panel2)}
.cutpanel .cphead{font-size:.95rem;margin-bottom:.5rem}
.cutpanel .prow{display:flex;align-items:center;gap:.6rem;margin:.35rem 0}
.cutpanel .prow label{flex:0 0 auto;min-width:150px}
.cutpanel .rngval{min-width:56px;text-align:right;color:var(--muted)}
.cutpanel .adv{display:inline-flex;align-items:center;gap:.4rem;margin:.4rem 0}
.cutpanel .cpsub{margin:.6rem 0 .2rem;font-size:.85rem;color:var(--muted);border-top:1px solid var(--line);padding-top:.5rem}
/* Multi-pass (catch-up) */
.mpass-toggle{display:inline-flex;align-items:center;gap:.4rem;margin:.6rem 0 0;font-weight:600}
.mppanel .mpheights{margin:.2rem 0}
.mppanel .mp_offrow{margin-top:.5rem;border-top:1px solid var(--line);padding-top:.5rem}
.mppanel #mp_hint{display:block;margin-top:.4rem}
/* Global / By-Zone tabs */
.cptabs{display:flex;gap:.4rem;margin:.2rem 0 .5rem}
.cptab{flex:1 1 0;padding:.5rem .6rem;border:1px solid var(--line);border-radius:8px;background:var(--panel2);
  color:var(--muted);font-size:.92rem;cursor:pointer}
.cptab.active{background:var(--accent);color:#04222a;border-color:var(--accent);font-weight:600}
.cphint{font-size:.82rem;color:var(--muted);margin:.1rem 0 .5rem;padding:.4rem .55rem;border-radius:8px;
  background:rgba(87,217,138,.10);border:1px solid rgba(87,217,138,.30)}
.cphint.warn{background:rgba(255,193,7,.12);border-color:rgba(255,193,7,.45);color:#e9c46a}
.cutpanel select,.cutpanel input[type=number]{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:.35rem .5rem}
/* inline cut-angle preview box (zone shapes + mowing strips, no map/satellite) */
.cppvrow{display:flex;align-items:center;gap:.6rem;margin:.35rem 0}
.cppvrow label{flex:0 0 auto;min-width:150px}
.cppreview{flex:1 1 auto;min-width:160px}
.cppvsvg{width:170px;height:auto;max-width:100%;background:#0d1117;border:1px solid var(--line);border-radius:8px;display:block}
.cppvcap{font-size:.78rem;color:var(--muted);margin-top:.2rem}
.cppvnote{font-size:.8rem;color:var(--muted);padding:.5rem;border:1px dashed var(--line);border-radius:8px}
/* multi-map run bar (lives on the Map tab, by the selection bar) */
.mmbar{display:flex;align-items:center;gap:.6rem;margin:.4rem 0;flex-wrap:wrap}
.scanout{margin-top:.5rem;padding:.6rem;background:var(--panel2);border:1px solid var(--line);border-radius:8px;
  font:12px/1.45 ui-monospace,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word;max-height:340px;overflow:auto}
/* multi-map live progress: one row per map-group, with state + a per-map mowed bar */
.mmprog{margin-top:.6rem;display:flex;flex-direction:column;gap:.4rem}
.mmrow{padding:.45rem .55rem;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}
.mmrow.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.mmrow.done{opacity:.75}
.mmrow .mmtop{display:flex;justify-content:space-between;gap:.5rem;font-size:.85rem}
.mmrow .mmst{color:var(--muted);font-size:.8rem}
.mmrow .mmst.s-mowing{color:var(--accent)}
.mmrow .mmst.s-done{color:#7ee0a6}
.mmrow .mmst.s-restoring{color:#e9c46a}
.pbar{height:8px;border-radius:6px;background:var(--panel2);border:1px solid var(--line);overflow:hidden;margin:.35rem 0}
.pbar span{display:block;height:100%;background:var(--accent);transition:width .5s}
.appver{margin-left:.5rem;font-size:.7rem;color:var(--muted);opacity:.8}
.zone-pick{cursor:pointer}
.mapsel{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin:.5rem 0 .2rem;font-size:.9rem}
.mapsel .zchip{background:rgba(87,217,138,.16);border:1px solid var(--accent);color:var(--text);border-radius:999px;padding:.12rem .55rem;font-size:.82rem}

/* ---- Maps panel: connected-mower banner, backup rows, zone checklist (mobile-first) ---- */
.nowmower{margin:.2rem 0 .6rem;font-size:.9rem;color:var(--muted)}
.nowmower b{color:var(--accent)}
.bklist{display:flex;flex-direction:column;gap:.4rem;margin:.3rem 0 .6rem}
.bklist .empty{color:var(--muted);font-size:.88rem;padding:.4rem 0}
.bkrow{display:flex;align-items:center;gap:.6rem;background:var(--panel2);border:1px solid var(--line);
  border-radius:10px;padding:.5rem .65rem;flex-wrap:wrap}
.bkrow .bkmeta{flex:1 1 160px;min-width:0}
.bkrow .bkname{font-size:.95rem;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bkrow .bkdate{font-size:.78rem;color:var(--muted)}
.bkrow .bkacts{display:flex;gap:.35rem;flex-wrap:wrap}
.bkrow .btn.small{flex:0 0 auto}
/* map thumbnails (current map + per restore point) */
.curmapbox{margin:.2rem 0 .7rem}
.mapthumbwrap{width:100%;max-width:260px;aspect-ratio:4/3;background:#0d1117;border:1px solid var(--line);
  border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center}
.mapthumb{width:100%;height:100%;display:block}
.thumbempty{color:var(--muted);font-size:.8rem}
.bkthumb{flex:0 0 auto;width:84px;height:63px;background:#0d1117;border:1px solid var(--line);
  border-radius:6px;overflow:hidden;display:flex;align-items:center;justify-content:center}
.bkthumb .thumbempty{font-size:.65rem;text-align:center}
@media (max-width:520px){.bkthumb{width:64px;height:48px}}
.zonechecks{display:flex;flex-direction:column;gap:.3rem;margin:.3rem 0 .6rem;
  max-height:230px;overflow:auto;border:1px solid var(--line);border-radius:10px;padding:.45rem .6rem}
.zonechecks .empty{color:var(--muted);font-size:.88rem}
.zonechecks label{display:flex;align-items:center;gap:.55rem;font-size:.95rem;cursor:pointer;padding:.2rem 0}
.zonechecks input[type=checkbox]{width:18px;height:18px;flex:0 0 auto}
@media(max-width:600px){
  .bkrow{align-items:flex-start}
  .bkrow .bkacts{width:100%}
  .bkrow .bkacts .btn{flex:1 1 auto}
}

/* ---- mow-order number badges on the map + backup zone chips ---- */
.ordnum{background:#0b3d22;color:#eafff2;border:2px solid #57d98a;border-radius:50%;
  width:22px;height:22px;line-height:18px;text-align:center;font-size:.78rem;font-weight:700;
  box-shadow:0 1px 4px rgba(0,0,0,.5)}
.zchip.bk{background:rgba(154,168,160,.22);border:1px solid #8a9a8f;color:var(--text)}

/* ---- floating control bar: pinned to the bottom of the viewport, always visible while scrolling ---- */
.floatbar{position:fixed;left:50%;transform:translateX(-50%);bottom:max(12px,env(safe-area-inset-bottom));
  z-index:1200;display:flex;gap:.5rem;padding:.5rem;border-radius:14px;
  background:rgba(18,24,20,.92);backdrop-filter:blur(8px);
  box-shadow:0 6px 24px rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.08);
  width:calc(100% - 24px);max-width:560px}
.floatbar .fbbtn{flex:1;min-width:0;padding:.7rem .4rem;border-radius:10px;border:1px solid transparent;
  font-size:.95rem;font-weight:700;cursor:pointer;color:#fff;background:#2c7a43;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.floatbar .fbbtn.primary{background:#2c7a43}
.floatbar .fbbtn.primary.danger{background:#1f6f8b}        /* Start mow (idle) reads as the go action */
.floatbar .fbbtn[data-mode=fault]{background:#b5742a}      /* Clear Fault */
.floatbar .fbbtn.danger{background:#8a2f2f}                /* Cancel Task */
.floatbar .fbbtn:active{filter:brightness(1.15)}
/* keep page content clear of the bar */
body.has-floatbar main{padding-bottom:84px}
.floatpending{min-height:1.1em;margin:.2rem 0 .4rem}
/* camera icon overlaid on the map (upper-right) */
.mapwrap{position:relative}
/* keep the map's +/- zoom buttons and the camera icon BELOW the centered status pill (it was covering them).
   The camera sits level with the + (zoom-in) button. The pill wraps to 2 rows on phones, so push further down there. */
.mapwrap .leaflet-top{top:42px}
.mapcam{position:absolute;top:52px;right:10px;z-index:600;width:38px;height:38px;border-radius:9px;
  border:1px solid rgba(255,255,255,.15);background:rgba(18,24,20,.82);color:#fff;font-size:1.1rem;
  cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.45)}
.mapcam:active{filter:brightness(1.2)}
/* v1.39 center-on-mower — same floating pill as the camera button, stacked directly under it, so
   finding the mower after panning around the property is one thumb tap. */
.mapcenter{position:absolute;top:98px;right:10px;z-index:600;width:38px;height:38px;border-radius:9px;
  border:1px solid rgba(255,255,255,.15);background:rgba(18,24,20,.82);color:#fff;font-size:1.1rem;
  cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.45)}
.mapcenter:active{filter:brightness(1.2)}
/* keep a map-control topic (toggle label) glued to its ⚙️ gear so they never wrap apart */
.ctlgrp{display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap;flex:0 0 auto}
/* map toolbar = LEFT section (Satellite / map controls / Clear trail / refresh) + RIGHT section (overlay
   toggles). Left grows, right hugs its content, so the toggles sit together on the right. */
.maptools{justify-content:space-between;align-items:flex-start}
.mtleft{display:flex;flex-wrap:wrap;align-items:center;gap:.7rem;flex:1 1 auto;min-width:0}
/* the 3 overlay toggles STACK vertically on the right; every row is the same width so the switches line up
   in a column, the ⚙️ gears line up in a column, and the label text is left-justified between them. */
.overlaytoggles{display:flex;flex-direction:column;align-items:stretch;gap:2px;flex:0 0 auto;margin:0}
.overlaytoggles .ctlgrp{display:flex;justify-content:space-between;align-items:center;gap:.7rem;white-space:nowrap}
.overlaytoggles .toggle{flex:1 1 auto;justify-content:flex-start;margin:0;padding:0;line-height:1.3}
.overlaytoggles .btn.ghost{flex:0 0 auto;padding:1px 6px;line-height:1.2}
/* zone freshness overlay legend (bottom-left of the map, only while the Freshness overlay is on) */
.stalelegend{position:absolute;left:10px;bottom:12px;z-index:600;max-width:min(72%,420px);
  background:rgba(18,24,20,.86);border:1px solid rgba(255,255,255,.15);border-radius:9px;
  padding:.4rem .55rem;font-size:.72rem;color:#e8f0e8;box-shadow:0 2px 8px rgba(0,0,0,.45);
  display:flex;flex-wrap:wrap;gap:.25rem .6rem;align-items:center;line-height:1.3}
.stalelegend b{margin-right:.2rem}
.stalelegend .slg{display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap}
.stalelegend .slg i{width:11px;height:11px;border-radius:3px;display:inline-block;flex:0 0 auto}
@media (max-width:560px){
  .mapwrap .leaflet-top{top:72px}   /* the status pill is 2 rows on small screens */
  .mapcam{top:82px}
  .mapcenter{top:128px}             /* follows .mapcam down when the status pill wraps to 2 rows */
  .stalelegend{left:6px;bottom:16px;font-size:.66rem;padding:.35rem .45rem}
}
@media (max-width:480px){ .floatbar .fbbtn{font-size:.85rem;padding:.65rem .3rem} }

/* ===================== Heat maps (inc5): selector, legend, hover readout, color pickers, drop log ===== */
.heatlbl{font-size:.72rem;color:#b8c4bc;padding:0 .1rem}
/* the heat legend sits ABOVE the freshness legend (bottom-left) so the two never overlap */
.heatlegend{bottom:52px;flex-direction:column;align-items:flex-start}
.heatlegend .hlrow{display:flex;flex-wrap:wrap;gap:.25rem .6rem;align-items:center;width:100%}
@media (max-width:560px){ .heatlegend{bottom:46px} }
/* floating hover readout — nearest heat cell's raw numbers. BOTTOM-RIGHT so it never covers the +/- zoom
   (top-left), the camera button (top-right), or the two bottom-left legends. */
.mapreadout{position:absolute;right:10px;bottom:22px;left:auto;top:auto;z-index:600;max-width:min(80%,460px);
  background:rgba(18,24,20,.9);border:1px solid rgba(255,255,255,.18);border-radius:9px;
  padding:.4rem .55rem;font-size:.72rem;color:#e8f0e8;box-shadow:0 2px 8px rgba(0,0,0,.5);
  display:flex;flex-wrap:wrap;gap:.25rem .6rem;align-items:center;line-height:1.3;pointer-events:none}
.mapreadout b{margin-right:.2rem}
.mapreadout .slg{display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap}
.mapreadout .slg i{width:11px;height:11px;border-radius:3px;display:inline-block;flex:0 0 auto}
/* color-picker box in the heat settings modals — small + centered, label directly over each swatch */
.colorgrid{margin:.4rem 0;padding:.35rem .4rem;border:1px solid rgba(255,255,255,.1);border-radius:9px;
  display:inline-block;width:auto;max-width:100%;text-align:center}
.colorgridt{font-size:.72rem;font-weight:600;color:#cdd8d0;margin:0 0 .25rem;text-align:center}
.prow.colorrow input[type=color]{width:40px;height:24px;padding:0;border:1px solid rgba(255,255,255,.2);
  border-radius:6px;background:transparent;cursor:pointer}
/* the 3 swatches sit on ONE centered row; each = a small CENTERED column of (label directly over swatch) */
.colorswatches{display:flex;flex-wrap:wrap;gap:.35rem .7rem;justify-content:center}
.colorswatches .prow.colorrow{flex:0 0 auto;flex-direction:column;align-items:center;gap:.15rem;margin:0;padding:0}
.colorswatches .prow.colorrow label{flex:0 0 auto;font-size:.7rem;color:#b8c4bc;justify-content:center;text-align:center}
/* Mowed-overlay group: the color swatch doubles as the "mowed square" beside the toggle, with
   the 🗑️ clear button on its right — one unified control group in the overlay-toggle row */
.trailswatch{width:26px;height:24px;padding:0;border:1px solid rgba(255,255,255,.2);
  border-radius:6px;background:transparent;cursor:pointer;flex:0 0 auto;vertical-align:middle}
.trashbtn{padding:.15rem .4rem;line-height:1;font-size:.95rem}
/* compact threshold group: one metric label, then its weak+drop SLIDERS on tight rows */
.thslide{margin:.4rem 0}
.thslide .thlbl{font-size:.78rem;color:#cdd8d0;font-weight:600;margin-bottom:.15rem}
.thsl{display:flex;align-items:center;gap:.5rem;margin:.15rem 0}
.thsl label{flex:0 0 46px;font-size:.74rem;color:#b8c4bc}
.thsl input[type=range]{flex:1 1 auto;min-width:80px}
.thsl .rngval{flex:0 0 auto;font-size:.74rem;color:#cdd8d0;min-width:54px;text-align:right}
/* drop log */
.droplog{margin-top:.6rem;max-height:110px;overflow:auto;border-top:1px solid rgba(255,255,255,.1);padding-top:.4rem}
.dl-head{font-size:.75rem;font-weight:600;color:#cdd8d0;margin-bottom:.3rem;display:flex;align-items:center}
.dl-row{font-size:.7rem;color:#dbe4dd;padding:.2rem 0;border-bottom:1px solid rgba(255,255,255,.06);
  display:flex;flex-wrap:wrap;gap:.25rem .5rem;align-items:baseline}
.dl-row .dl-t{color:#9fb0a4}
.dl-row .dl-loc{color:#8fb8ff}
.dl-row .dl-v{color:#b8c4bc}
.dl-bad{color:#ff2ec4;font-weight:700}
.dl-warn{color:#9b5de5;font-weight:700}
.dl-ok{color:#06d6a0;font-weight:700}

/* ===================== Calendar (inc3) ===================== */
.calbar{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:center;justify-content:space-between;margin-bottom:.6rem}
.calnav{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}
.caltitle{font-size:1.05rem;margin-left:.4rem}
.calviews{display:flex;align-items:center;gap:.3rem}
.calview.active{background:var(--accent,#3fb950);color:#04210c;border-color:transparent}
.calgrid{width:100%}
.calgrid.calmonth,.calgrid.calweek{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
.calhd{font-size:.72rem;color:var(--muted);text-align:center;padding:.2rem 0;font-weight:600}
.calcell{min-height:88px;background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:8px;padding:.25rem;display:flex;flex-direction:column;gap:.2rem;overflow:hidden}
.calcell.dim{opacity:.45}
.calcell.today{border-color:var(--accent,#3fb950);box-shadow:inset 0 0 0 1px var(--accent,#3fb950)}
.calnum{font-size:.72rem;color:var(--muted);font-weight:600}
.calchips{display:flex;flex-direction:column;gap:2px;overflow:hidden}
.calnone{color:var(--line,#26302a);font-size:.7rem}
.calchip{border:0;border-radius:5px;padding:.12rem .3rem;font-size:.68rem;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#04210c}
.calchip.green{background:#3fb950}
.calchip.orange{background:#f0883e}
.calchip.red{background:#f85149;color:#fff}
.calchip:active{filter:brightness(1.15)}
.calgrid.calweek .calcell{min-height:120px}
.calgrid.calday{display:flex;flex-direction:column;gap:6px}
.calrow{display:grid;grid-template-columns:auto 1fr auto auto;gap:.6rem;align-items:center;text-align:left;
  background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-left-width:4px;border-radius:8px;padding:.5rem .7rem;cursor:pointer;color:var(--text,#e8f0e8);font-size:.85rem}
.calrow.green{border-left-color:#3fb950}
.calrow.orange{border-left-color:#f0883e}
.calrow.red{border-left-color:#f85149}
.calrt{font-weight:700}
.calrs{color:var(--muted)}
.calra{color:var(--muted);font-size:.8rem}
@media (max-width:560px){
  .calrow{grid-template-columns:auto 1fr;gap:.2rem .5rem}
  .calra{grid-column:1/3}
  .calcell{min-height:70px}
  .calchip{font-size:.6rem}
}
/* mow-detail modal */
.mowmodal{display:flex;gap:1rem;flex-wrap:wrap}
.mmthumb{flex:0 0 auto;width:240px;height:180px;background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:10px;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.mmthumb .mapthumb{width:100%;height:100%}
.mmnote{position:absolute;bottom:4px;left:0;right:0;text-align:center;font-size:.66rem;color:var(--muted)}
.mmfacts{flex:1 1 220px;min-width:200px;display:flex;flex-direction:column;gap:.45rem}
.mmwhen{font-weight:600}
.mmbadge{display:inline-block;align-self:flex-start;border-radius:6px;padding:.15rem .5rem;font-size:.8rem;font-weight:700;color:#04210c}
.mmbadge.green{background:#3fb950}
.mmbadge.orange{background:#f0883e}
.mmbadge.red{background:#f85149;color:#fff}
.mmrow{display:flex;justify-content:space-between;gap:1rem;border-top:1px solid var(--line,#26302a);padding-top:.4rem}
.mmrow>span{color:var(--muted)}
.mzchips{display:flex;flex-wrap:wrap;gap:.25rem;justify-content:flex-end}
.mzchip{background:var(--panel2,#141a16);border:1px solid var(--line,#26302a);border-radius:5px;padding:.05rem .35rem;font-size:.78rem}
.mzchip.green{background:#3fb950;border-color:#3fb950;color:#04210c}   /* per-zone freshness: each cut zone its OWN color */
.mzchip.orange{background:#f0883e;border-color:#f0883e;color:#04210c}
.mzchip.red{background:#f85149;border-color:#f85149;color:#fff}
@media (max-width:560px){ .mmthumb{width:100%} }

/* HEADLESS on-box Google sign-in browser (streamed) — HA / Docker / Ubuntu Server */
.gstream{position:fixed;inset:0;z-index:4000;background:rgba(4,7,12,.82);
  display:flex;align-items:center;justify-content:center;padding:16px}
.gstreamcard{background:#0d1117;border:1px solid var(--line);border-radius:12px;
  box-shadow:0 18px 60px rgba(0,0,0,.5);max-width:520px;width:100%;overflow:hidden}
.gstreamhdr{display:flex;align-items:center;justify-content:space-between;
  padding:.55rem .8rem;border-bottom:1px solid var(--line)}
.gstreamttl{font-weight:600;font-size:.92rem}
.gstreamx{background:transparent;border:0;color:var(--muted);font-size:1rem;cursor:pointer;
  line-height:1;padding:.2rem .4rem;border-radius:6px}
.gstreamx:hover{background:#1b2028;color:#e6edf3}
.gstreamwrap{position:relative;background:#fff;min-height:200px}
.gstreamimg{display:block;width:100%;height:auto;cursor:default;user-select:none;-webkit-user-select:none;outline:none}
/* PHONE keyboard bridge: a transparent editable input laid exactly over the streamed image so a
   phone will raise its soft keyboard when the user taps a field. Invisible (transparent text/caret),
   but really focusable. app.js unhides it only on touch devices and forwards the tap as a click. */
.gstreamkbd{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;background:transparent;
  color:transparent;caret-color:transparent;font-size:16px;/* >=16px stops iOS auto-zoom */
  -webkit-text-fill-color:transparent;outline:none;z-index:2;touch-action:none}
/* "⌨ Type" toggle: shown only on touch devices (app.js unhides it); .on = keyboard is up. */
.gstreamkbdbtn{margin-left:auto;margin-right:.4rem;background:var(--panel2);color:var(--text);
  border:1px solid var(--line);border-radius:8px;padding:.25rem .55rem;font-size:.82rem;cursor:pointer}
.gstreamkbdbtn.on{background:var(--accent,#2ea043);color:#fff;border-color:transparent}
.gstreamover{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  color:var(--muted);background:#0d1117;font-size:.9rem;text-align:center;padding:1rem}
.gstreammsg{padding:.5rem .8rem;color:var(--muted);font-size:.8rem;border-top:1px solid var(--line)}

/* inc11 — RTK logs panel + header LoRa chip */
.lorachip{cursor:default;background:#1c2530;color:#9fb6c9;border-color:#2c3a48}
.rtklwrap{max-height:170px;overflow:auto;border:1px solid var(--line,#2c3a48);border-radius:8px;margin-top:.5rem}
.rtkltable{width:100%;border-collapse:collapse;font-size:.78rem;font-variant-numeric:tabular-nums}
.rtkltable th{position:sticky;top:0;background:var(--panel,#141a21);cursor:pointer;user-select:none;
  padding:.3rem .45rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--line,#2c3a48)}
.rtkltable td{padding:.18rem .45rem;white-space:nowrap;border-bottom:1px solid rgba(128,128,128,.12)}
.rtkltable tbody tr:hover{background:rgba(128,160,255,.07)}
/* v1.40 "Forgot password?" — quiet secondary action under the Unlock button; deliberately
   understated so it reads as a recovery path, not an alternative way in. */
.gforgot{margin-top:.6rem;font-size:.9rem;opacity:.85}
/* v1.40.0-inc7: the reset is its OWN gate screen (title + warning + Reset + Back), so the unlock
   form stays clean. Its warning is the standard #gatesub, just tinted to read as a caution. */
#gate .gatesub-warn{color:var(--warn)}
/* v1.40.0-inc6: cross-cut is set inside the multi-pass panel itself, sitting under Perimeter (edge) */
.mp_crosstoggle{display:block;margin:.5rem 0 .2rem}
/* v1.41: 📖 glossary — in the header beside the version, so it is reachable on every tab and also
   when the LAN bar is hidden (viewing through the remote link). */
.glossbtn{background:none;border:0;cursor:pointer;font-size:1.05rem;line-height:1;padding:.1rem .25rem;
  opacity:.85;filter:grayscale(.15)}
.glossbtn:hover{opacity:1;filter:none}
/* Update-available badge. HIDDEN unless a NEWER release was actually found — same version, a
   downgrade, or being offline all leave it invisible, so its presence always means something. */
.updbtn{animation:updpulse 1.4s ease-in-out infinite;filter:none;opacity:1}
@keyframes updpulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.9)}}
/* A blinking element is exactly what vestibular/attention accessibility settings ask us to stop. */
@media (prefers-reduced-motion:reduce){ .updbtn{animation:none} }

/* Update progress — the server is DOWN for most of this, so these steps are the only thing on
   screen telling the user the machine is busy rather than broken. */
.updsteps{list-style:none;margin:.6rem 0 .8rem;padding:0}
.updsteps li{display:flex;align-items:center;gap:.7rem;padding:.4rem 0;color:var(--muted)}
.updsteps li .ust-ic{width:1.05rem;height:1.05rem;border-radius:50%;flex:0 0 auto;
  border:2px solid var(--line);box-sizing:border-box}
.updsteps li.active{color:var(--text)}
.updsteps li.active .ust-ic{border-color:var(--accent);border-top-color:transparent;
  animation:uspin .8s linear infinite}
.updsteps li.done{color:var(--text)}
.updsteps li.done .ust-ic{border-color:var(--accent);background:var(--accent)}
@keyframes uspin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){ .updsteps li.active .ust-ic{animation:none} }

/* The screen left behind after Shut down. It REPLACES the whole app, so it can rely on no layout
   above it — and it must use the theme, not hardcoded colours, or it renders as bare text jammed
   against one corner. That is exactly how it shipped: one line, then half a line down and to the
   right. */
.shutwrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
  background:var(--bg);padding:2rem;text-align:center;z-index:9999}
.shutbox{max-width:34rem}
.shuttitle{color:var(--text);font-size:1.25rem;font-weight:600;margin-bottom:.9rem;line-height:1.4}
.shutsub{color:var(--muted);font-size:.95rem;line-height:1.55;margin-top:.5rem}
.shutdim{opacity:.7;font-size:.88rem;margin-top:1rem}
.shutcode{display:inline-block;margin-top:.4rem;padding:.35rem .6rem;border-radius:6px;
  background:var(--panel2);border:1px solid var(--line);color:var(--text);
  font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.9rem;user-select:all}

/* Settings side rail — ten panels in one scroll was the complaint. */
.setwrap{display:flex;gap:1rem;align-items:flex-start}
.setnav{flex:0 0 190px;display:flex;flex-direction:column;gap:.25rem;position:sticky;top:.5rem}
.setnavbtn{text-align:left;background:transparent;color:var(--muted);border:1px solid transparent;
  border-radius:8px;padding:.5rem .7rem;font:inherit;font-size:.9rem;cursor:pointer}
.setnavbtn:hover{background:var(--panel2);color:var(--text)}
.setnavbtn.on{background:var(--panel2);color:var(--text);border-color:var(--line);
  box-shadow:inset 3px 0 0 var(--accent)}
.setbody{flex:1 1 auto;min-width:0}
/* On a phone the rail becomes a scrollable row of chips — a 190px column would eat the screen. */
@media (max-width:760px){
  .setwrap{flex-direction:column;gap:.5rem}
  .setnav{flex:0 0 auto;flex-direction:row;overflow-x:auto;width:100%;position:static;
    padding-bottom:.25rem;-webkit-overflow-scrolling:touch}
  .setnavbtn{white-space:nowrap;flex:0 0 auto}
  .setnavbtn.on{box-shadow:inset 0 -3px 0 var(--accent)}
  .setbody{width:100%}
}
/* The update modal is deliberately wider than the standard 560px one: it carries release
   highlights that read badly in a narrow column. */
.modalbox.wide{max-width:min(80vw, 900px)}
.updhl{margin:.2rem 0 .9rem;padding-left:1.1rem}
.updhl li{margin:.3rem 0;line-height:1.45}
.updver{color:var(--muted);font-size:.85rem;margin:-.3rem 0 .8rem}
.upddonate{border-top:1px solid var(--line);margin-top:1rem;padding-top:.8rem;font-size:.9rem;color:var(--muted)}
.updcmd{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:.6rem .7rem;
  font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.85rem;overflow-x:auto;white-space:pre}
.glosssearch{width:100%;padding:.55rem .7rem;margin-bottom:.6rem;border-radius:10px;
  border:1px solid var(--line);background:var(--panel);color:var(--text);font-size:.95rem}
.glossec{margin:.9rem 0 .3rem;font-weight:700;color:var(--accent);font-size:.9rem}
.glossterm{margin:.45rem 0;line-height:1.4}
.glossterm b{color:var(--text)}
.glossterm span{color:var(--muted);font-size:.9rem}
.glossnone{color:var(--muted);font-size:.9rem;padding:.6rem 0}
/* v1.41: the glossary is a reference table, not a dialog — 85% of the screen so definitions read on
   a line or two instead of a narrow ribbon. The search + close + section jumps STICK to the top so
   you never scroll to the bottom to get out, or lose the search while reading. */
#modal.glosswide .modalbox{max-width:85vw}
.glosshead{position:sticky;top:0;z-index:5;background:var(--panel);padding:.1rem 0 .5rem;
  margin:-.2rem 0 .4rem;border-bottom:1px solid var(--line);
  display:grid;grid-template-columns:1fr auto;gap:.4rem .5rem;align-items:center}
.glosshead .glosssearch{margin:0}
.glossx{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:10px;
  min-width:44px;min-height:44px;font-size:1rem;cursor:pointer}
.glossx:hover{border-color:var(--accent);color:var(--accent)}
.glossjump{grid-column:1 / -1;display:flex;flex-wrap:wrap;gap:.3rem}
.glossjbtn{background:var(--panel2);color:var(--muted);border:1px solid var(--line);border-radius:999px;
  padding:.28rem .7rem;font-size:.82rem;cursor:pointer;white-space:nowrap}
.glossjbtn:hover{color:var(--accent);border-color:var(--accent)}
#modal.glosswide .glossbody{columns:1}   /* one column — easier to read across */
#modal.glosswide .glossec{break-after:avoid;break-inside:avoid;scroll-margin-top:6rem}
#modal.glosswide .glossterm{break-inside:avoid}
@media (max-width:900px){ #modal.glosswide .modalbox{max-width:94vw} }

/* v1.41: a setting's OPTIONS, listed under it — what each choice actually does. */
.glossopts{margin:.3rem 0 .5rem 1.1rem;border-left:2px solid var(--line);padding-left:.7rem}
.glossopt{margin:.22rem 0;font-size:.93rem}
.glossopt i{color:var(--accent);font-style:normal;font-weight:600}
.glossopt span{color:var(--muted)}

/* v1.42 Home Assistant bridge — a hostname doesn't fit the default 130px .prow input, and the
   status line needs its own good/bad colors (the existing ones are scoped to cards/telemetry). */
#hamqtt .prow input[type=text],#hamqtt .prow input[type=password]{width:220px;flex:1 1 220px;max-width:340px}
#hamqtt .hasub{margin:.6rem 0}
#hamqtt .hasub>summary{color:var(--muted);font-size:.9rem}
#ha_state{margin-top:.6rem}
#ha_state .good{color:var(--accent)}
#ha_state .bad{color:var(--bad)}

/* v1.42.1 FULLSCREEN MAP — mirrors the camera's proven .fs pattern (.camwrap.fs / body.camfs-lock)
   rather than the browser Fullscreen API, so it behaves identically to something already field-tested.
   z1000 deliberately sits BELOW the modal (1300), tooltip (3500) and toast/flashok (4500) so a
   confirmation can never end up hidden behind the map — and below the float bar (1200), which unlike
   the camera we KEEP visible: selecting zones then pressing Start mow is the whole point of the map. */
.mapfsbtn{position:absolute;top:8px;right:10px;z-index:600;width:38px;height:38px;border-radius:9px;
  border:1px solid var(--line);background:rgba(18,24,20,.86);color:var(--text);font-size:1.05rem;
  line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
.mapfsbtn:hover{border-color:var(--accent)}
.mapexitfs{position:absolute;top:8px;right:10px;z-index:1001;background:rgba(0,0,0,.55);display:none}
.mapwrap.fs{position:fixed;top:0;left:0;z-index:1000;width:100vw;height:100vh;height:100dvh;
  max-width:100vw;border-radius:0;margin:0;background:#0d1117}
.mapwrap.fs #leafletmap{height:100vh;height:100dvh;border-radius:0;border:0}
.mapwrap.fs .mapexitfs{display:block}
.mapwrap.fs .mapfsbtn{display:none}
body.mapfs-lock{overflow:hidden}
/* the overlay toggles live in .maptools, a SIBLING of .mapwrap — once .mapwrap is position:fixed they
   would be stranded behind it, leaving fullscreen a dead end you can't change anything from. Float the
   SAME nodes over the map instead of moving them in the DOM, so their existing handlers keep working. */
body.mapfs-lock .overlaytoggles{position:fixed;top:8px;left:8px;z-index:1002;
  background:rgba(18,24,20,.92);backdrop-filter:blur(8px);padding:.45rem .6rem;border-radius:12px;
  border:1px solid rgba(255,255,255,.08);box-shadow:0 6px 24px rgba(0,0,0,.5);max-width:min(94vw,320px)}
@media (max-width:560px){
  body.mapfs-lock .overlaytoggles{font-size:.82rem;padding:.35rem .45rem}
}

/* v1.42.1 — the overlay toggles docked INTO a camera fullscreen box. They are physically moved here
   (see dockOverlayToggles): CSS alone cannot work for the camera because #tab-overview is display:none
   while you are on the Remote tab, and a position:fixed descendant of a display:none element does not
   render. Absolute, because the fullscreen wrapper is already position:fixed.

   PLACEMENT IS LOAD-BEARING — field report v1.42.1-inc1: "the toggles cover up the indicator for
   whether WiFi or 4G is working." The camera's top-left corner is already occupied by .camstatus AND
   .camled (both at top:.5rem/left:.6rem), and .camtelbar — which carries the WiFi and 4G icons — is
   CENTERED at the top. At top:8px this sat on all three.
     * top:52px clears the 34px light button (8px + 34px + margin) and the status chip.
     * The width cap is the part that actually fixes the WiFi/4G occlusion: at the old
       min(60vw,300px) a ~500px-wide window put this at x=8..308 while the centered telbar spanned
       ~125..375, so they overlapped from 125 to 308 — which is why it looked intermittent (on a wide
       desktop window they never touched). 38vw keeps the right edge clear of a centered bar at any
       width, including when that bar wraps to two rows.
   z-index stays 1002 — ABOVE the mini-map (7) and the map itself so the toggles remain clickable.
   This was a POSITIONING bug, not a stacking one; do not "fix" a future overlap by burying it. */
.overlaytoggles.ovdock{position:absolute;top:52px;left:8px;z-index:1002;
  background:rgba(18,24,20,.92);backdrop-filter:blur(8px);padding:.45rem .6rem;border-radius:12px;
  border:1px solid rgba(255,255,255,.08);box-shadow:0 6px 24px rgba(0,0,0,.5);
  max-width:min(38vw,260px);font-size:.85rem}
.overlaytoggles.ovdock .btn.ghost{padding:0 5px}
@media (max-width:560px){
  /* landscape phone: smaller, and narrower still so the centered telemetry bar stays readable */
  .overlaytoggles.ovdock{font-size:.75rem;padding:.3rem .4rem;max-width:44vw;gap:0}
}

/* v1.43.0 — away-access switch in the header bar, beside the QR button */
.lanbar .toggle.lanext{margin:0;font-size:.85rem;white-space:nowrap}
.lanbar .toggle.lanext input:disabled ~ .slider{opacity:.45;cursor:not-allowed}
/* greyed out because the viewer came in THROUGH the away link — switching it off there would cut
   their own connection. Only the OFF direction is locked; see wireExternalAccess(). */
.lanbar .toggle.lanext.locked{opacity:.55;cursor:not-allowed}
.lanbar #extmsg{font-size:.8rem}

/* ---------- kiosk mode (Home Assistant frame) ----------
   PRESENTATION ONLY. Hides the chrome that makes no sense inside a card — the brand header, the
   LAN/away bar and the tab strip — so the frame is the remote view and nothing else. It grants
   nothing: access is the remote token, checked server-side on every request. The float bar STAYS,
   because Dock / Cancel / Mow belong with the joystick. */
body.kiosk > header,
body.kiosk #lanbar,
body.kiosk nav.tabs,
body.kiosk #a2hsbar{display:none!important}
/* Every pixel of the frame belongs to the picture. The card's height is a fixed ratio of its width
   (an iframe card has no height setting), so page padding and a redundant heading come straight out
   of the camera — on a phone frame that is the difference between the whole control fitting and it
   needing a scroll. The ⚠️ blade warning STAYS: it is the one line here that is about safety. */
body.kiosk main{padding:.35rem .5rem}
body.kiosk .panel{padding:.4rem .55rem;margin-bottom:.55rem;border-radius:10px}
body.kiosk #tab-remote > .panel:first-child > h3{display:none}
body.kiosk .camwrap{margin:.25rem 0}
/* landscape auto-fullscreen is deliberately off in the card (it would hide the float bar and take
   Dock/Cancel/Mow with it), so the hint that tells you to rotate would be a lie here */
body.kiosk .rchint{display:none}
/* the card sizes the iframe, so the page itself must never add a second scrollbar around it */
body.kiosk{overflow-x:hidden}

/* The screen left behind after Shut down. It replaces the whole app, so it cannot rely on any
   layout above it — and it must use the theme rather than hardcoded colours, or it renders as
   unstyled text on a white page in one theme and is unreadable in the other. */
.shutwrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
  background:var(--bg);padding:2rem;text-align:center}
.shutbox{max-width:34rem}
.shuttitle{color:var(--text);font-size:1.25rem;font-weight:600;margin-bottom:.9rem}
.shutsub{color:var(--muted);font-size:.95rem;line-height:1.55;margin-top:.5rem}
.shutdim{opacity:.7;font-size:.88rem;margin-top:1rem}
.shutcode{display:inline-block;margin-top:.4rem;padding:.35rem .6rem;border-radius:6px;
  background:var(--panel2);border:1px solid var(--line);color:var(--text);
  font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.9rem;user-select:all}

/* ---- fullscreen remote controls (v1.44.1) ---------------------------------------------------
   Driving by hand used to mean leaving the picture to change the deck or the blades. These live on
   the video itself and appear ONLY in fullscreen, laid out for landscape thumbs: blade speed on the
   right under the joystick side, drive speed and deck height across the bottom middle. */
/* Sizes come from sizeRcFs() in app.js, measured from the same base as the joystick and from the
   joystick's own diameter — so enlarging the controller shrinks these and pushes them clear instead
   of ending up underneath it. The fallbacks here are only for the first paint. */
.rcfsblades{position:absolute;right:.5rem;bottom:var(--rcfsbbot,200px);z-index:1002;
  display:none;flex-direction:column;gap:6px}
.camwrap.fs .rcfsblades,.camwrap:fullscreen .rcfsblades,.camwrap.pseudofs .rcfsblades{display:flex}
/* no room above the joystick -> lay them across the top instead of sliding under it */
.camwrap.rcfs-row .rcfsblades{flex-direction:row;bottom:auto;top:.5rem;right:.5rem;left:auto}
.rcfsb{background:rgba(0,0,0,.55);color:#cfe8c4;border:1px solid rgba(255,255,255,.18);
  border-radius:8px;padding:0 .4rem;width:var(--rcfsbw,58px);height:var(--rcfsbh,34px);
  font-size:var(--rcfsbf,.8rem);line-height:1;cursor:pointer}
.rcfsb:hover{background:rgba(0,0,0,.72)}
.rcfsb.on{background:#2e7d32;color:#fff;border-color:#4caf50;font-weight:600}
.rcfsboff{color:#ffb4a9}
.rcfsboff.on{background:#7a2f28;border-color:#c0554a;color:#fff}
.rcfsbar{position:absolute;left:50%;bottom:.5rem;transform:translateX(-50%);z-index:1002;
  display:none;flex-direction:column;gap:.25rem;align-items:center;
  background:rgba(0,0,0,.55);border-radius:10px;padding:.4rem .7rem;
  max-width:min(var(--rcfsbarmax,70vw),520px)}
.camwrap.fs .rcfsbar,.camwrap:fullscreen .rcfsbar,.camwrap.pseudofs .rcfsbar{display:flex}
.rcfsctl{display:flex;align-items:center;gap:.5rem;color:#cfe8c4;font-size:.8rem;white-space:nowrap}
.rcfslbl{min-width:42px}
.rcfsctl input[type=range]{width:min(calc(var(--rcfsbarmax,70vw) - 130px),240px);min-width:90px}
.rcfsctl .rngval{min-width:64px;text-align:right}
.rcfsmsg{color:#9fd18a;font-size:.75rem;min-height:1em}

/* Remote tab settings rows: the value BEFORE the slider, and never wrapping (v1.44.1).
   #tab-remote is a two-column grid at >=860px with the right column only 300-380px wide, so
   label(min 180px) + slider + value did not fit and .prow's flex-wrap dropped the value onto its own
   line. Reordering is CSS-only — the markup keeps the value after the input, where every other panel
   in the app expects it. */
#tab-remote .prow{flex-wrap:nowrap}
#tab-remote .prow label{flex:0 1 auto;min-width:0}
#tab-remote .prow .rngval{order:2;flex:0 0 auto;min-width:58px;text-align:right}
#tab-remote .prow input[type=range]{order:3;flex:1 1 60px;min-width:0;width:auto}

/* A custom update feed (LYMOW_UPDATE_FEED) silently disabling real updates — say so loudly. */
.warnline{margin-top:.4rem;padding:.4rem .6rem;border-radius:8px;font-size:.85rem;
  background:rgba(255,180,60,.12);border:1px solid rgba(255,180,60,.45);color:#ffc86b}

/* A setting that is locked because another one owns it — visibly not editable, still readable. */
.toggle.locked{opacity:.65}
.toggle.locked .slider{cursor:not-allowed}

/* The name in the header links to the community group. display:contents so the anchor adds NO box of
   its own — the logo and "Toolkit" stay exactly where the flex layout already put them, and the only
   visible change is the cursor. rel="noopener" because target="_blank" without it hands the opened
   page a reference back to this one. */
.brandlink{display:contents;color:inherit;text-decoration:none}
.brandlink:hover .brandsub{text-decoration:underline}
