*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#111;--bg2:#1c1c1c;--bg3:#252525;--border:#2e2e2e;--text:#eee;--muted:#808080;--orange:#e05a22;--green:#5c9e3a;--red:#c0392b;--yellow:#e0b422;
      --kam-hi:#3a3a3a;--kam-lo:#161616;--kam-edge:#4a4a4a;--kam-metal:#5a5a5a;
      --kam-dark:#242424;--kam-void:#141414;--kam-grate:#6a5240;--kam-sheen:rgba(255,255,255,.10)}
[data-theme="minimal"]{
  --bg:#f0e8dc;--bg2:#fdf7ef;--bg3:#e8ddd0;
  --border:#c8b89a;--text:#2a1a0a;--muted:#8a7060;
  --orange:#c05818;--green:#3a7a20;--red:#b02020;--yellow:#8a6a00;
}
body{background:var(--bg);color:var(--text);font-family:'Outfit',-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh;transition:background .35s,color .35s}
/* Smooth transitions for theme switch */
.card,.ac,.hc,.hdr,#alert-bar{transition:background .35s,border-color .35s,color .35s}

/* Login. The sign-in card does not exist until the lid is open -- the state
   table is in PLAN-login-lid.md, the toggle is toggleLid() in js/app-core.js. */
#login{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:20px;padding:2rem;padding-top:max(2rem, env(safe-area-inset-top));padding-bottom:max(2rem, env(safe-area-inset-bottom));padding-left:max(2rem, env(safe-area-inset-left));padding-right:max(2rem, env(safe-area-inset-right));position:relative}
/* Warm wash over the viewport, standing in for the reference's animated body
   background. Kept off body{} so it cannot fight the theme transition on line
   10. It is positioned, so the in-flow children need a layer of their own. */
#login::before{content:'';position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0;transition:opacity .6s ease;background:radial-gradient(58% 44% at 50% 40%,color-mix(in srgb,var(--orange) 20%,transparent),color-mix(in srgb,var(--orange) 6%,transparent) 45%,transparent 72%)}
#login>*{position:relative;z-index:1}
#login.kam-open::before{opacity:1}

/* DESIGN-SYSTEM.md §3: touch targets >=44px. The SVG is far larger than that,
   but the min-height is stated so a later size change cannot silently break it. */
#kam-toggle{background:none;border:none;padding:0;min-height:44px;cursor:pointer;-webkit-tap-highlight-color:transparent}
/* DESIGN-SYSTEM.md §4: the focus ring is 2px/2px everywhere. Not negotiable per screen. */
#kam-toggle:focus-visible{outline:2px solid var(--orange);outline-offset:2px;border-radius:12px}
.kam-svg{width:100%;max-width:240px;height:auto;overflow:visible;display:block}

/* Every colour in the illustration, in one place -- DESIGN-SYSTEM.md §1 and the
   grep in §7.1. The SVG carries no fill or stroke attribute at all. */
.kam-s-hi{stop-color:var(--kam-hi)}
.kam-s-lo{stop-color:var(--kam-lo)}
.kam-bowl,.kam-dome{stroke:var(--kam-edge);stroke-width:1.5}
.kam-band{fill:var(--kam-hi);stroke:var(--kam-metal);stroke-width:1}
.kam-leg{stroke:var(--kam-dark);stroke-width:6;stroke-linecap:round;fill:none}
.kam-rail,.kam-handle{fill:var(--kam-dark)}
.kam-vent{fill:var(--kam-void);stroke:var(--kam-metal);stroke-width:1}
.kam-damper{fill:var(--kam-hi);stroke:var(--kam-metal);stroke-width:1}
.kam-cap{fill:var(--kam-hi)}
.kam-hinge{fill:var(--kam-metal)}
.kam-sheen{fill:none;stroke:var(--kam-sheen);stroke-width:7;stroke-linecap:round}
.kam-grate{stroke:var(--kam-grate);stroke-width:1.5;fill:none}
/* The fire IS the accent colour. Derived, never literal -- on the light
   minimal theme a hard-coded ember hex vanishes and --orange does not. */
.kam-e-hot{stop-color:color-mix(in srgb,var(--orange) 40%,white)}
.kam-e-mid{stop-color:var(--orange)}
.kam-e-edge{stop-color:color-mix(in srgb,var(--red) 65%,black)}
.kam-f-tip{stop-color:color-mix(in srgb,var(--orange) 35%,white)}
.kam-f-mid{stop-color:var(--orange)}
.kam-f-base{stop-color:color-mix(in srgb,var(--red) 50%,var(--orange))}
.kam-g-in{stop-color:var(--orange);stop-opacity:.55}
.kam-g-out{stop-color:var(--orange);stop-opacity:0}
.kam-puff{fill:var(--muted);opacity:0}

/* Positive angle: the hinge is on the RIGHT, so clockwise lifts the left side.
   A negative angle drives the dome into the floor. See PLAN-login-lid.md §2. */
.kam-lid{transform-origin:159px 104px;transition:transform .55s cubic-bezier(.34,1.28,.5,1)}
#login.kam-open .kam-lid{transform:rotate(38deg)}
.kam-fire,.kam-glow{opacity:0;transition:opacity .5s ease .12s}
#login.kam-open .kam-fire,#login.kam-open .kam-glow{opacity:1}

/* Nothing animates while the lid is shut. A cold smoker is the resting state,
   and it costs no compositor work on a screen a signed-out user may sit on. */
#login.kam-open .kam-flame{transform-origin:center bottom;animation:onb-flicker 1.4s ease-in-out infinite}
.kam-flame2{animation-delay:.35s}
.kam-flame3{animation-delay:.7s}
#login.kam-open .kam-glow{animation:kam-breathe 3.2s ease-in-out infinite}
@keyframes kam-breathe{0%,100%{opacity:.85}50%{opacity:1}}
#login.kam-open .kam-puff{animation:onb-rise 3.6s ease-out infinite}
.kam-puff2{animation-delay:1.2s}
.kam-puff3{animation-delay:2.4s}

.kam-hint{font-size:13px;color:var(--muted);transition:opacity .3s ease}
#login.kam-open .kam-hint{opacity:0;pointer-events:none}

/* visibility, not just opacity: an opacity-0 card is still in the tab order.
   `inert` says the same thing to assistive tech, but it is the newer of the
   two and this app has to hold up on an older iOS Safari. The 0s visibility
   transition is delayed past the fade-out (.18 + .45) so the card does not
   vanish mid-dissolve, and is undelayed on the way in. */
#login-card{display:flex;flex-direction:column;align-items:center;gap:16px;width:100%;max-width:320px;background:var(--bg2);border:.5px solid var(--border);border-radius:16px;padding:20px 24px;box-shadow:0 8px 32px rgba(0,0,0,.1);visibility:hidden;opacity:0;transform:translateY(14px) scale(.97);pointer-events:none;transition:opacity .45s ease .18s,transform .45s ease .18s,visibility 0s linear .63s}
#login.kam-open #login-card{visibility:visible;opacity:1;transform:none;pointer-events:auto;transition:opacity .45s ease .18s,transform .45s ease .18s,visibility 0s}
.appname{font-size:28px;font-weight:600}
.tagline{font-size:14px;color:var(--muted);text-align:center;max-width:260px}
#gbtn{display:flex;align-items:center;gap:12px;background:#fff;color:#333;border:none;border-radius:10px;padding:13px 24px;font-size:15px;font-weight:500;cursor:pointer;width:100%;max-width:280px;justify-content:center}
#gbtn:hover{opacity:.9}
#gbtn:active{transform:scale(.98)}
#errmsg{font-size:12px;color:#e57373;text-align:center;max-width:280px;display:none}
/* Anyone who has asked the OS not to animate lands on the open page, fully
   assembled, with no motion at all -- the same contract as line 246. The JS
   half (opening the lid on load) is not enough on its own: without this block
   the lid still swings, the coals still flicker and the hint still shows. */
/* Each selector below MIRRORS the one that declares the motion, because every
   animation here is declared on `#login.kam-open .kam-*` and a bare `.kam-*`
   loses to it on specificity -- the first version of this block silently did
   nothing but stop the lid transition. Same reason #login-card is listed twice. */
@media (prefers-reduced-motion:reduce){
  #login::before,.kam-lid,.kam-fire,.kam-glow,.kam-hint{transition:none}
  #login-card,#login.kam-open #login-card{transition:none}
  #login.kam-open .kam-flame,#login.kam-open .kam-puff,#login.kam-open .kam-glow{animation:none}
  .kam-hint{display:none}
}

/* App */
#app{display:none;max-width:600px;margin:0 auto;padding:1.5rem}
.hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;position:relative;flex-wrap:wrap;gap:12px}
.hdrl{display:flex;align-items:center;gap:8px}
.dot{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .4s;flex-shrink:0}
.dot.on{background:var(--green)}
/* No device linked: neither a fault (red) nor a live connection (green).
   Muted, because there is nothing here for the user to go fix — see
   setNoDeviceStatus() in js/dashboard.js. */
.dot.none{background:var(--muted)}
.hdrt{font-size:16px;font-weight:500}
.hdrr{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.stxt{font-size:11px;color:var(--muted)}
.sbtn{font-size:12px;color:var(--muted);background:none;border:.5px solid var(--border);border-radius:6px;padding:5px 10px;cursor:pointer}
.sbtn:hover{color:var(--text)}

/* WiFi Signal Chip */
.wifi-chip{display:inline-flex;align-items:center;gap:4px;color:var(--muted);transition:color .2s ease;cursor:default}
.wifi-chip.is-good{color:var(--green)}
.wifi-chip.is-fair{color:var(--yellow)}
.wifi-chip.is-weak{color:var(--red)}
.wifi-chip.is-connected{color:var(--green);opacity:.85}
.wifi-chip.is-unknown{color:var(--muted);opacity:.4}
.wifi-chip.is-weak .wifi-arc-3,.wifi-chip.is-weak .wifi-arc-2{opacity:.2}
.wifi-chip.is-fair .wifi-arc-3{opacity:.2}
.wifi-chip.is-unknown .wifi-arc-3,.wifi-chip.is-unknown .wifi-arc-2{opacity:.2}

/* Base Battery Chip (GATT 0x2A19, PLAN-base-battery.md) */
.batt-chip{display:inline-flex;align-items:center;gap:3px;color:var(--muted);transition:color .2s ease;cursor:default}
.batt-chip.is-good{color:var(--green)}
.batt-chip.is-fair{color:var(--yellow)}
/* Floating panel inside a card (TODO #295; DESIGN-SYSTEM.md §4): the card recipe, anchored by JS under its link. */
.card-popover{position:absolute;left:12px;right:12px;z-index:30;background:var(--bg2);border:.5px solid var(--border);border-radius:16px;padding:12px 16px 16px;box-shadow:0 8px 32px rgba(0,0,0,.25);max-height:min(60vh,420px);overflow:auto;text-align:start}
.card-popover[hidden]{display:none}
/* The host card goes above its sibling cards while its panel is open: the panel's own z-index only counts inside the card. Above the fixed header (90), below menus and modals (100+). */
.card.has-popover{z-index:95}
.card-popover-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
.card-popover-title{font-size:13px;font-weight:600;color:var(--text)}
.card-popover-close{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;margin-inline-end:-12px;padding:0;background:none;border:none;border-radius:10px;color:var(--muted);cursor:pointer;transition:color .2s ease}
.card-popover-close:hover{color:var(--text)}
.card-popover-body{margin:4px 0 0;padding-inline-start:18px;font-size:12px;line-height:1.5;color:var(--text)}
.card-popover-body li+li{margin-top:6px}
.card-popover-more{display:inline-block;margin-top:10px;font-size:12px;color:var(--orange)}
.card-popover-link{display:inline-flex;align-items:center;min-height:44px;padding:0;background:none;border:none;font:inherit;font-size:12px;color:var(--orange);text-decoration:underline;cursor:pointer}
.card-popover-close:focus-visible,.card-popover-link:focus-visible{outline:2px solid var(--orange);outline-offset:2px}
.batt-chip.is-low{color:var(--red)}
.batt-chip.is-unknown{color:var(--muted);opacity:.4}

/* BLE Probe Signal Chip */
.probe-signal-chip{display:inline-flex;align-items:center;gap:4px;color:var(--muted);transition:color .2s ease}
.probe-signal-chip.is-excellent,.probe-signal-chip.is-good,.probe-signal-chip.is-connected{color:var(--green)}
.probe-signal-chip.is-docked{color:var(--green)}
.probe-signal-chip.is-fair{color:var(--yellow)}
.probe-signal-chip.is-weak{color:var(--red)}
.probe-signal-chip.is-unknown{color:var(--muted);opacity:.4}
.probe-signal-chip.is-weak .sig-bar-2,.probe-signal-chip.is-weak .sig-bar-3,.probe-signal-chip.is-weak .sig-bar-4{opacity:.2}
.probe-signal-chip.is-fair .sig-bar-3,.probe-signal-chip.is-fair .sig-bar-4{opacity:.2}
.probe-signal-chip.is-good .sig-bar-4{opacity:.2}
.probe-signal-chip.is-excellent .sig-bar-1,.probe-signal-chip.is-excellent .sig-bar-2,.probe-signal-chip.is-excellent .sig-bar-3,.probe-signal-chip.is-excellent .sig-bar-4{opacity:1}
.probe-signal-chip.is-connected .sig-bar-1,.probe-signal-chip.is-connected .sig-bar-2,.probe-signal-chip.is-connected .sig-bar-3,.probe-signal-chip.is-connected .sig-bar-4{opacity:1}
.probe-signal-chip.is-docked .sig-bar-1,.probe-signal-chip.is-docked .sig-bar-2,.probe-signal-chip.is-docked .sig-bar-3,.probe-signal-chip.is-docked .sig-bar-4{opacity:1}

/* Cards */
.cards{display:grid;grid-template-columns:1fr;gap:12px;margin-bottom:12px;perspective:1200px}
.card{position:relative;background:var(--bg2);border:.5px solid var(--border);border-radius:16px;padding:20px 24px;min-width:0;box-shadow:0 8px 32px rgba(0,0,0,0.1);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);transition:transform 0.3s cubic-bezier(0.25,0.8,0.25,1), box-shadow 0.3s cubic-bezier(0.25,0.8,0.25,1);transform-style:preserve-3d;will-change:transform}
.card:hover{transform:translateY(-2px);box-shadow:0 12px 40px rgba(0,0,0,0.15)}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:38%;background:linear-gradient(180deg,rgba(255,255,255,0.035) 0%,transparent 100%);pointer-events:none;border-radius:16px 16px 0 0}
.full{grid-column:1}

/* 3D Slot Machine Reel Roll for Cards */
@keyframes cardSlotReel{
  0%{opacity:0.3;transform:perspective(1000px) rotateX(28deg) translateY(-20px);box-shadow:0 20px 40px rgba(0,0,0,0.3)}
  65%{opacity:1;transform:perspective(1000px) rotateX(-5deg) translateY(3px)}
  85%{transform:perspective(1000px) rotateX(2deg) translateY(-1px)}
  100%{opacity:1;transform:perspective(1000px) rotateX(0deg) translateY(0);box-shadow:0 8px 32px rgba(0,0,0,0.1)}
}
.card.card-slot-spin{animation:cardSlotReel 0.65s cubic-bezier(0.16,1,0.3,1) var(--card-reel-delay,0s) both}
#pit-card.card-slot-spin{--card-reel-delay:0s}
#meat-card.card-slot-spin{--card-reel-delay:0.14s}
#temp-chart-card.card-slot-spin{--card-reel-delay:0.24s}
.cards > .card.full:nth-of-type(n+4).card-slot-spin{--card-reel-delay:0.32s}

/* Simple reel look (native scroll, 2026-09-05): perspective only -- cards
   stay in normal flow, JS paints just rotateX/scale/opacity per card. */
#tab-dashboard > .cards.reel-look{perspective:1100px}
#tab-dashboard > .cards.reel-look > .card{transform-style:preserve-3d;will-change:transform,opacity}
/* Drum snap without a drum engine: the browser snaps, JS only paints. Body-
   scoped so other tabs keep their own scrolling. Proximity, not mandatory --
   mandatory fights the user on long cards (fan) and on programmatic scrolls. */
body.reel-snap #tab-dashboard > .cards.reel-look > .card{scroll-snap-align:center}
body.reel-snap{scroll-snap-type:y proximity}
/* Mobile breathing room for the reel: tilted cards project beyond their
   layout box, which eats the 12px grid gap and reads as touching. Base is
   mobile (desktop rules live under min-width:700px elsewhere in this file). */
#tab-dashboard > .cards{row-gap:20px}
@media (min-width:700px){#tab-dashboard > .cards{row-gap:12px}}

/* Subtle vertical reel pager dots on the right side */
.slot-drum-pager{
  position:fixed;
  right:8px;
  top:50%;
  transform:translateY(-50%);
  display:flex;
  flex-direction:column;
  gap:8px;
  padding:8px 5px;
  background:rgba(20,20,24,0.75);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  border:0.5px solid var(--border);
  border-radius:12px;
  z-index:90;
  transition:opacity 0.3s ease;
}
.slot-drum-pager-dot{
  width:6px;
  height:6px;
  border-radius:50%;
  background:rgba(255,255,255,0.25);
  cursor:pointer;
  transition:background 0.25s ease, transform 0.25s ease, height 0.25s ease, border-radius 0.25s ease;
}
.slot-drum-pager-dot.active{
  background:var(--orange);
  height:16px;
  border-radius:4px;
  box-shadow:0 0 8px rgba(224,90,34,0.6);
}

/* Wider viewports: pit/meat cards sit side by side; chart/fan stay full width */
@media (min-width:700px){
  #app{max-width:840px}
  .cards{grid-template-columns:1fr 1fr}
  .full{grid-column:1/-1}
}

/* Per-probe mini-graph grid (live + history split views) -- its own 2-column
   grid nested inside the full-width strip, one row per pair of probes, an
   odd probe (or the "other probes" pool card) trailing alone on its own
   row. Single column below the same 700px breakpoint everything else uses. */
#probe-split-charts,#hist-probe-split-charts{grid-column:1;display:grid;grid-template-columns:1fr;gap:12px}
@media (min-width:700px){
  #probe-split-charts,#hist-probe-split-charts{grid-column:1/-1;grid-template-columns:1fr 1fr}
}
.clbl{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-weight:500}
.cv{font-size:40px;font-weight:400;line-height:1;min-width:0;display:inline-flex;align-items:baseline}

/* Directional Odometer & Number Roll (Up/Down) */
.slot-digit{position:relative;display:inline-block;vertical-align:baseline;height:1em;line-height:1;overflow:hidden;perspective:250px}
.slot-digit .slot-char{display:inline-block;height:1em;line-height:1;transform-style:preserve-3d;will-change:transform,opacity}

/* Roll UP (value increased: old digit rolls up, new digit rolls in from bottom) */
.slot-digit.roll-up::before{content:attr(data-old);position:absolute;left:0;top:0;width:100%;height:1em;line-height:1;color:inherit;pointer-events:none;animation:rollUpOld var(--roll-duration,0.85s) cubic-bezier(0.2,0.85,0.3,1) var(--delay,0s) forwards}
.slot-digit.roll-up .slot-char{animation:rollUpNew var(--roll-duration,0.85s) cubic-bezier(0.2,0.85,0.3,1) var(--delay,0s) forwards}

@keyframes rollUpOld{
  0%{transform:translateY(0) rotateX(0deg);opacity:1;filter:blur(0px)}
  100%{transform:translateY(-105%) rotateX(55deg);opacity:0;filter:blur(var(--roll-blur,0px))}
}
@keyframes rollUpNew{
  0%{transform:translateY(105%) rotateX(-55deg);opacity:0;filter:blur(var(--roll-blur,0px))}
  75%{transform:translateY(-3%) rotateX(2deg);opacity:1;filter:blur(0px)}
  100%{transform:translateY(0) rotateX(0deg);opacity:1;filter:blur(0px)}
}

/* Roll DOWN (value decreased: old digit rolls down, new digit rolls in from top) */
.slot-digit.roll-down::before{content:attr(data-old);position:absolute;left:0;top:0;width:100%;height:1em;line-height:1;color:inherit;pointer-events:none;animation:rollDownOld var(--roll-duration,0.85s) cubic-bezier(0.2,0.85,0.3,1) var(--delay,0s) forwards}
.slot-digit.roll-down .slot-char{animation:rollDownNew var(--roll-duration,0.85s) cubic-bezier(0.2,0.85,0.3,1) var(--delay,0s) forwards}

@keyframes rollDownOld{
  0%{transform:translateY(0) rotateX(0deg);opacity:1;filter:blur(0px)}
  100%{transform:translateY(105%) rotateX(-55deg);opacity:0;filter:blur(var(--roll-blur,0px))}
}
@keyframes rollDownNew{
  0%{transform:translateY(-105%) rotateX(55deg);opacity:0;filter:blur(var(--roll-blur,0px))}
  75%{transform:translateY(3%) rotateX(-2deg);opacity:1;filter:blur(0px)}
  100%{transform:translateY(0) rotateX(0deg);opacity:1;filter:blur(0px)}
}
.dg{font-size:18px;color:var(--muted);margin-left:2px}
.age-lbl{font-size:11px;color:var(--muted);vertical-align:middle}
.pit{color:var(--orange)}
.trow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:14px;padding-top:14px;border-top:.5px solid var(--border)}
/* TODO #301: the pit and meat cards both end with their Target row (and its "set"
   note), so side by side the two rows line up whatever sits above them -- the pit
   card's IR/calibration lines, the meat card's ETA. The grid already gives the two
   cards equal height; ::after is a spacer that grows to fill the difference and
   keeps the old 14px gap as its floor (.card::before is absolute, so it is not a
   flex item). Status banners that used to follow the row now sit above it. */
#pit-card,#meat-card{display:flex;flex-direction:column}
#pit-card::after,#meat-card::after{content:'';order:1;flex:1 0 14px}
#pit-card>.trow,#meat-card>.trow{order:2;margin-top:0}
#pt-note,#mt-note{order:3}
.tlbl{font-size:11px;color:var(--muted);flex-shrink:0}
.ti{width:60px;flex:0 0 60px;min-width:0;padding:13px 4px;border:.5px solid var(--border);border-radius:8px;background:var(--bg3);color:var(--text);font-size:15px;font-weight:500;text-align:center;-webkit-appearance:none}
.ti:focus{outline:none;border-color:var(--orange)}
.ti:disabled{opacity:.4}
.val-pending{opacity:.4}
button:disabled{opacity:.5;cursor:default}
.tb{padding:6px 14px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;border:.5px solid var(--border);border-radius:10px;background:var(--bg3);color:var(--text);font-size:12px;cursor:pointer;white-space:nowrap;transition:all 0.2s ease;box-shadow:0 2px 8px rgba(0,0,0,0.05)}
/* touch-action/user-select/callout: these are hold-to-repeat buttons (see the
   pointerdown delegation in app-core.js). Without them a long press on mobile
   selects the +/- glyph or opens the iOS callout instead of repeating. */
.step{width:44px;height:44px;border:.5px solid var(--border);border-radius:10px;background:var(--bg3);color:var(--text);font-size:17px;cursor:pointer;line-height:1;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:all 0.2s ease;box-shadow:0 2px 8px rgba(0,0,0,0.05);touch-action:manipulation;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.step:hover{background:#333;transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.1)}
.step:active{transform:translateY(1px) scale(0.95);box-shadow:0 1px 4px rgba(0,0,0,0.05)}
.tb:hover{background:#333;transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.1)}
.tb:active{transform:translateY(1px) scale(0.98);box-shadow:0 1px 4px rgba(0,0,0,0.05)}
.tb.active{background:var(--orange);color:#fff;border-color:var(--orange)}
/* Round delete button on a History session card. It lives INSIDE the card's
   own click target, so history.js stops propagation on it -- styling it as a
   distinct circle is what makes that second target legible. Muted until
   touched, then red: destructive, but not shouting from a list of cooks.
   Theme vars only (not .tb's hardcoded #333 hover) -- this sits on var(--bg3)
   cards that go light in the minimal theme. */
.hist-del{width:36px;height:36px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;padding:0;border:.5px solid var(--border);border-radius:50%;background:var(--bg2);color:var(--muted);cursor:pointer;transition:background .2s ease,color .2s ease,border-color .2s ease,transform .2s ease;touch-action:manipulation}
.hist-del:hover{background:var(--red);border-color:var(--red);color:#fff}
.hist-del:active{transform:scale(0.92)}
.hist-del:disabled{opacity:.45;cursor:default;transform:none;background:var(--bg2);border-color:var(--border);color:var(--muted)}
.device-tabs{display:flex;gap:6px;padding:8px 16px;overflow-x:auto;border-bottom:.5px solid var(--border)}
.device-tab{padding:6px 14px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;border:.5px solid var(--border);border-radius:10px;background:var(--bg3);color:var(--text);font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all 0.2s ease}
.device-tab:hover{background:#333;transform:translateY(-1px)}
.device-tab.active{background:var(--orange);color:#fff;border-color:var(--orange)}
.probe-pit-list,.probe-meat-list{margin-top:8px;font-size:12px;color:var(--muted);display:flex;gap:14px;flex-wrap:wrap}
.pit-mode-row,.meat-mode-row{margin-top:8px;display:flex;gap:6px}
.fr{display:flex;align-items:baseline;justify-content:space-between}
.fs{font-size:12px;color:var(--muted)}
.bb{height:7px;background:var(--bg3);border-radius:4px;overflow:hidden;margin-top:10px}
.bar{height:7px;border-radius:4px;background:linear-gradient(90deg, var(--orange), #ff8833);transition:width .6s cubic-bezier(0.4, 0, 0.2, 1);width:0%;box-shadow:0 0 8px rgba(224, 90, 34, 0.5)}

/* Alerts */
.ac{background:var(--bg2);border:.5px solid var(--border);border-radius:16px;padding:20px 24px;margin-bottom:12px;box-shadow:0 8px 32px rgba(0,0,0,0.1);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);transition:transform 0.3s ease, box-shadow 0.3s ease}
.ar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px;padding-top:12px;border-top:.5px solid var(--border)}
.ar:first-of-type{margin-top:6px;padding-top:0;border-top:none}
.ar .al{font-size:12px;color:var(--muted);flex-shrink:0;width:70px}
.ar input[type=number]{width:68px;flex-shrink:0;padding:5px 8px;border:.5px solid var(--border);border-radius:7px;background:var(--bg3);color:var(--text);font-size:13px;text-align:center;-webkit-appearance:none;transition:opacity .2s}

/* Toggle switch */
/* margin-inline-start, not margin-left: this auto margin is what pins the
   toggle to the far END of its flex row. In RTL the row reverses but the
   physical left margin does not, so it absorbed the free space on the
   toggle's outer side and shoved it back against the number input --
   measured at 420px wide: LTR toggle flush to the row edge (0px), RTL
   toggle 120px short of it with 10px to the input. The RTL block at the
   bottom of this file already flips .tog-sl::before's own left/right; the
   wrapper's margin was the one physical property in this control missed. */
.tog{position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0;margin-inline-start:auto}
.tog input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer;z-index:2;top:0;left:0}
.tog-sl{position:absolute;inset:0;background:#444;border-radius:12px;transition:background .2s;pointer-events:none}
.tog-sl::before{content:'';position:absolute;width:18px;height:18px;background:#fff;border-radius:50%;top:3px;left:3px;transition:transform .3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow .2s;box-shadow:0 1px 3px rgba(0,0,0,0.3)}
.tog input:checked + .tog-sl{background:var(--orange)}
.tog input:checked + .tog-sl::before{transform:translateX(20px);box-shadow:0 0 8px rgba(255,255,255,0.6)}
.svbtn{width:100%;margin-top:10px;padding:10px;border:.5px solid var(--border);border-radius:10px;background:linear-gradient(145deg, var(--bg3), var(--bg2));color:var(--text);font-size:14px;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:0 4px 12px rgba(0,0,0,0.1)}
.svbtn:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,0,0,0.15)}

/* OTA */
.ota-btn{width:100%;padding:10px;border:none;border-radius:10px;background:linear-gradient(145deg, var(--green), #4a8030);color:#fff;font-size:14px;font-weight:600;cursor:pointer;margin-top:8px;transition:all 0.2s ease;box-shadow:0 4px 12px rgba(92, 158, 58, 0.3)}
.ota-btn:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(92, 158, 58, 0.4)}
.ota-btn:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}

/* ── First-run / no-device onboarding (js/onboarding.js) ──────────────────────
   While the account has no linked smoker, every other child of the dashboard
   tab is hidden: those cards, target inputs and control buttons all describe a
   device that does not exist, and half of them write to devices/{deviceId}/…
   with deviceId still null. #onboard-view is the single exemption. */
#tab-dashboard.no-device > *:not(#onboard-view){display:none!important}

.onb-hero{display:flex;justify-content:center;margin:8px 0 18px}
.onb-smoker{width:100%;max-width:260px;height:auto;overflow:visible}
/* Three puffs on one path, staggered — reads as a continuous plume rather
   than three balls, and costs one compositor layer each. */
.onb-puff{fill:#9aa4b4;opacity:0;animation:onb-rise 3.6s ease-out infinite}
.onb-puff2{animation-delay:1.2s}
.onb-puff3{animation-delay:2.4s}
@keyframes onb-rise{
  0%{opacity:0;transform:translate(0,0) scale(.35)}
  20%{opacity:.32}
  70%{opacity:.16}
  100%{opacity:0;transform:translate(-11px,-30px) scale(1.35)}
}
.onb-flame{transform-origin:67px 112px;animation:onb-flicker 1.4s ease-in-out infinite}
@keyframes onb-flicker{
  0%,100%{transform:scaleY(1) scaleX(1);opacity:.95}
  35%{transform:scaleY(1.16) scaleX(.94);opacity:1}
  70%{transform:scaleY(.92) scaleX(1.06);opacity:.85}
}
.onb-needle{transform-origin:122px 95px;animation:onb-sweep 6s ease-in-out infinite}
@keyframes onb-sweep{0%,100%{transform:rotate(-52deg)}50%{transform:rotate(58deg)}}
.onb-probe{stroke-dasharray:56;stroke-dashoffset:56;animation:onb-draw 6s ease-in-out infinite}
@keyframes onb-draw{0%,10%{stroke-dashoffset:56}45%,100%{stroke-dashoffset:0}}
/* Anyone who has asked the OS not to animate gets the illustration still. */
@media (prefers-reduced-motion:reduce){
  .onb-puff,.onb-flame,.onb-needle,.onb-probe{animation:none}
  .onb-probe{stroke-dashoffset:0}
  .onb-puff{opacity:.35}
}

.onb-title{font-size:21px;font-weight:600;text-align:center;margin-bottom:8px}
.onb-sub{font-size:13px;color:var(--muted);text-align:center;max-width:420px;margin:0 auto 20px;line-height:1.5}
.onb-actions{display:flex;flex-direction:column;gap:10px;max-width:420px;margin:0 auto}
.onb-btn{display:flex;align-items:center;justify-content:center;gap:9px;width:100%;min-height:48px;padding:12px 18px;border:.5px solid var(--border);border-radius:12px;background:var(--bg2);color:var(--text);font-size:14px;font-weight:500;font-family:inherit;cursor:pointer;transition:all .2s ease}
.onb-btn svg{width:17px;height:17px;flex-shrink:0}
.onb-btn:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,0,0,.18)}
.onb-btn:active{transform:translateY(1px) scale(.99)}
.onb-btn-primary{background:linear-gradient(145deg,var(--orange),#b8410f);border-color:var(--orange);color:#fff;box-shadow:0 4px 12px rgba(224,90,34,.3)}
.onb-btn-primary:hover{box-shadow:0 6px 18px rgba(224,90,34,.42)}
/* Busy state for a control that is waiting on the network. The generic
   button:disabled rule dims to 50%, which all but erases the spinner, so a
   busy control needs to outrank it -- a disabled button and a working button
   have to look different. */
.onb-btn.is-busy:disabled{opacity:.85;cursor:progress}
.btn-spinner{width:16px;height:16px;flex-shrink:0;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:spin .8s linear infinite}
/* Reduced motion still needs to signal "working" — a frozen ring reads as stuck. */
@media (prefers-reduced-motion:reduce){.btn-spinner{animation:btn-pulse 1s ease-in-out infinite}}
@keyframes btn-pulse{0%,100%{opacity:1}50%{opacity:.3}}
.onb-link{display:block;margin:16px auto 4px;background:none;border:none;color:var(--muted);font-size:12px;font-family:inherit;text-decoration:underline;cursor:pointer;min-height:44px}
.onb-link:hover{color:var(--text)}
/* Second link in the same stack — .onb-link's 16px top margin on top of the
   previous one's 44px tap target leaves a hole between them. */
#onboard-signout{margin-top:0}

/* ── Device discovery in flight (js/app-core.js discoverDevice) ──────────────
   Same exemption pattern as .no-device: nothing on the dashboard means
   anything until the account's device list has actually been read. */
#tab-dashboard.discovering > *:not(#connect-view){display:none!important}
/* The nav bar lives outside the dashboard panel, hence the class on <body>.
   Every tab it offers is as unusable as the dashboard while the device list
   is unknown: History reads the same database that isn't answering, Alerts
   and Monitor describe a smoker nobody has looked up, and offering them makes
   the app look like it is working. The panel's own Retry is the way out. */
body.discovering .nav-bar{display:none!important}
/* On no-device: show bottom nav with Monitor (onboarding) and History; hide device-specific tabs */
body.no-device .nav-bar{justify-content:center}
body.no-device .nav-bar .nav-item{max-width:180px}
body.no-device .nav-bar #nav-alerts,
body.no-device .nav-bar #nav-chat,
body.no-device .nav-bar #nav-instructions{display:none!important}
/* Header on no-device: keep language, theme, and account trigger [ S ⌵ ]; hide device-specific hardware controls */
body.no-device #unit-btn,
body.no-device #bell-btn,
body.no-device #menu-btn{display:none!important}
/* The onboarding shown on a guess rather than a lookup (see
   showNoDeviceOnboarding's `unconfirmed`). Deliberately quiet -- it qualifies
   the screen, it isn't an error the user must act on. */
.onb-warn{display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;max-width:420px;margin:0 auto 14px;padding:8px 12px;border-radius:10px;background:rgba(224,160,32,.1);border:.5px solid rgba(224,160,32,.28);font-size:12px;color:#c8a020;line-height:1.45;text-align:center}
.onb-warn-btn{background:none;border:.5px solid rgba(224,160,32,.5);border-radius:7px;color:#c8a020;font-size:12px;font-family:inherit;padding:4px 10px;cursor:pointer}
.onb-warn-btn:hover{background:rgba(224,160,32,.14)}
.conn-spin{width:34px;height:34px;margin:46px auto 20px;border:2.5px solid var(--border);border-top-color:var(--orange);border-radius:50%;animation:conn-spin 1s linear infinite}
@keyframes conn-spin{to{transform:rotate(360deg)}}
/* Reduced motion still needs a "working" cue, so it pulses instead of spins. */
@media (prefers-reduced-motion:reduce){
  .conn-spin{animation:conn-pulse 1.6s ease-in-out infinite;border-top-color:var(--border)}
  @keyframes conn-pulse{0%,100%{opacity:.3}50%{opacity:.9}}
}
/* Once it's an error, the spinner stops claiming progress it isn't making. */
#connect-view.conn-failed .conn-spin{animation:none;border-top-color:var(--red);border-color:var(--red);opacity:.5}

/* ── Linked but offline (js/onboarding.js showDeviceOffline) ─────────────────
   Narrower than .no-device above: the smoker EXISTS, so the device tabs, the
   header and the nav all stay. What goes is the row of "–" cards and every
   control that would write a command to a device that is not listening
   (Target/Set, Start smoker, Stop blower, Smolder…).

   The Temperature card is the one card kept, because by the time this class is
   set _hydrateOfflineSession() has filled it with the cook this smoker last
   ran — but only when that load actually produced points, hence the
   .has-last-cook pair. Without it, a smoker that never recorded anything would
   trade a dead dashboard for an equally empty chart. */
#tab-dashboard.device-offline .cards > .card:not(#temp-chart-card){display:none!important}
#tab-dashboard.device-offline #probe-split-charts,
#tab-dashboard.device-offline #probe-identity,
#tab-dashboard.device-offline #start-smoker-hint,
#tab-dashboard.device-offline #boot-resume-banner,
#tab-dashboard.device-offline #sched-bar{display:none!important}
#tab-dashboard.device-offline:not(.has-last-cook) #temp-chart-card{display:none!important}

.off-smoker{width:100%;max-width:260px;height:auto;overflow:visible;opacity:.85}
/* The only movement: three dots looking for a device that isn't answering.
   A cold smoker with a flickering flame would be a lie. */
.off-dot{fill:#6a7382;opacity:.25;animation:off-seek 1.8s ease-in-out infinite}
.off-dot2{animation-delay:.3s}
.off-dot3{animation-delay:.6s}
@keyframes off-seek{0%,100%{opacity:.18}50%{opacity:.7}}
@media (prefers-reduced-motion:reduce){.off-dot{animation:none;opacity:.4}}
.off-lastcook{font-size:12px;color:var(--muted);text-align:center;max-width:420px;margin:18px auto 0}

.onb-feats{display:flex;flex-direction:column;gap:12px;max-width:420px;margin:22px auto 0;padding-top:18px;border-top:.5px solid var(--border)}
.onb-feat{display:flex;align-items:flex-start;gap:11px;font-size:12px;color:var(--muted);line-height:1.45}
.onb-feat strong{color:var(--text);font-weight:500}
.onb-feat-ico{font-size:17px;line-height:1.2;flex-shrink:0}

/* Setup-token modal — centered, unlike the .ah-panel drawers: it is a single
   value to read and copy, not a list to browse. */
.token-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.96);width:min(360px,92vw);max-height:88vh;overflow-y:auto;background:var(--bg);border:.5px solid var(--border);border-radius:16px;z-index:180;box-shadow:0 18px 50px rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity .22s,transform .22s}
.token-modal.open{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
.token-body{padding:18px 20px max(20px, env(safe-area-inset-bottom))}
/* user-select:all makes one tap select the whole token — the token is the only
   thing on this screen the user needs off it. LTR + tabular figures so an
   8-char code is not reordered inside the Hebrew layout or rendered ragged. */
.token-value{user-select:all;-webkit-user-select:all;direction:ltr;unicode-bidi:isolate;text-align:center;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:27px;font-weight:600;letter-spacing:.16em;padding:16px 10px;margin-bottom:14px;border-radius:12px;background:var(--bg3);border:.5px solid var(--border);color:var(--text);word-break:break-all;cursor:pointer}
.token-modal.is-pending .token-value{display:flex;align-items:center;justify-content:center;gap:10px;min-height:66px;font-family:inherit;font-size:14px;font-weight:500;letter-spacing:normal;color:var(--muted);cursor:progress;pointer-events:none;user-select:none;-webkit-user-select:none}
.token-modal.is-pending #token-copy-btn,.token-modal.is-pending .token-expiry{visibility:hidden}
.token-spinner{width:22px;height:22px;flex-shrink:0;border:2.5px solid var(--border);border-top-color:var(--orange);border-radius:50%;animation:spin .8s linear infinite}
/* Reduced motion substitutes a pulse: a frozen ring reads as stuck. */
@media (prefers-reduced-motion:reduce){.token-spinner{animation:btn-pulse 1s ease-in-out infinite}}
.token-help{font-size:12px;color:var(--muted);line-height:1.55;margin-top:14px}
.token-expiry{font-size:11px;color:var(--muted);margin-top:8px;opacity:.8}

/* Demo cook */
.demo-ribbon{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:12px;padding:4px 14px;border-radius:10px;font-size:12px;color:#c8a020;background:rgba(200,160,32,.1);border:.5px solid rgba(200,160,32,.3)}
.demo-sub{font-size:11px;color:var(--muted);margin-top:8px}

/* History empty state — a list with nothing in it has to say so plainly, and
   say what to do next; it used to be one muted line that read like a stalled
   load. */
.hist-empty{text-align:center;padding:26px 12px 20px;color:var(--muted)}
.hist-empty svg{width:34px;height:34px;opacity:.45;margin-bottom:12px}
.hist-empty-title{font-size:14px;font-weight:500;color:var(--text);margin-bottom:6px}
.hist-empty-sub{font-size:12px;line-height:1.5;max-width:300px;margin:0 auto}

/* History */
.hc{background:var(--bg2);border:.5px solid var(--border);border-radius:16px;padding:20px 24px;margin-bottom:12px;box-shadow:0 8px 32px rgba(0,0,0,0.1);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);transition:transform 0.3s ease, box-shadow 0.3s ease}
canvas{width:100%!important}
/* Graph gestures: PC wheel zooms (chartjs-plugin-zoom), mobile pinch zooms
   (hammer). One finger must scroll the PAGE, not pan the chart -- the plugin
   sets inline touch-action:none via hammer, so this needs !important to win.
   Safe: maximum-scale=1 in the viewport meta means the browser never claims
   the two-finger stream for page pinch-zoom, so hammer keeps it. */
canvas{touch-action:pan-y !important}

/* Theme toggle */
.theme-btn{font-size:16px;background:var(--bg3);color:var(--text);border:.5px solid var(--muted);border-radius:10px;padding:9px 10px;cursor:pointer;line-height:1;transition:all 0.2s ease}
.theme-btn:hover{border-color:var(--text);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.1)}

/* Persistent alert */
#alert-bar{display:none;background:#7a1a1a;color:#ffcdd2;padding:10px 16px;font-size:13px;font-weight:500;text-align:center;border-radius:8px;margin-bottom:12px}
#alert-bar.show{display:block}
.sysbar{display:none;padding:10px 16px;font-size:13px;font-weight:500;text-align:center;border-radius:8px;margin-bottom:12px;background:var(--red);color:#ffcdd2}
.sysbar.show{display:block}
.sysbar.sysbar-muted{background:var(--bg3);color:var(--muted)}

/* Loading */
#loading{display:none;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:16px}
.spinner{width:36px;height:36px;border:3px solid var(--border);border-top-color:var(--orange);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.loading-txt{font-size:13px;color:var(--muted)}

/* Banner */
#bnr{display:none;position:fixed;bottom:calc(70px + env(safe-area-inset-bottom) + .75rem);right:max(1.5rem, env(safe-area-inset-right));padding:10px 16px;font-size:13px;font-weight:500;z-index:100;text-align:left;border-radius:10px;max-width:280px;box-shadow:0 4px 20px rgba(0,0,0,.5)}
#bnr.show{display:block}
#bnr.warn{background:#7a4a1a;color:#ffe0b2}
/* Errors sit a step above warnings. Added with the device error channel --
   showBanner(msg,'err') previously matched no rule at all, so it produced a
   visible-but-unstyled box (only #bnr.show applied). */
#bnr.err{background:#a11c1c;color:#fff;border:1px solid #d64545}
#bnr.info{background:#1a3a1a;color:#c8e6c9}

/* The delete-device modal that used to live here was removed with the legacy
   page in d9e13b7; its CSS was left behind and styled nothing for months.
   Deleted 2026-08-08 — found by tests/test_css_selectors.py, which is the
   first check this repo has that would notice. */

/* ── AI Chat ─────────────────────────────────────────────────────────────────── */
#chat-msgs{height:420px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:10px}
#chat-msgs::-webkit-scrollbar{width:3px}
#chat-msgs::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
.cmsg{display:flex;flex-direction:column;max-width:86%}
.cmsg.user{align-self:flex-end;align-items:flex-end}
.cmsg.ai{align-self:flex-start}
.cmsg-bubble{padding:8px 12px;border-radius:12px;font-size:13px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
.cmsg.user .cmsg-bubble{background:var(--orange);color:#fff;border-bottom-right-radius:3px}
.cmsg.ai .cmsg-bubble{background:var(--bg3);color:var(--text);border:.5px solid var(--border);border-bottom-left-radius:3px}
.chat-inp-row{display:flex;gap:8px;align-items:flex-end}
.chat-inp{flex:1;padding:8px 12px;border:.5px solid var(--border);border-radius:10px;background:var(--bg3);color:var(--text);font-size:14px;resize:none;overflow:hidden;min-height:38px;max-height:130px;line-height:1.4;font-family:inherit}
.chat-inp:focus{outline:none;border-color:var(--orange)}
.chat-send{padding:0 16px;height:38px;border:none;border-radius:10px;background:var(--orange);color:#fff;font-size:14px;font-weight:500;cursor:pointer;flex-shrink:0}
.chat-send:hover{opacity:.9}
.chat-send:disabled{opacity:.45;cursor:default}
.chat-footer-row{display:flex;justify-content:space-between;margin-top:6px}
.chat-sm-btn{font-size:11px;color:var(--muted);background:none;border:none;cursor:pointer;padding:0}
.chat-sm-btn:hover{color:var(--text)}
.cmsg.ai .cmsg-bubble *{margin:0;padding:0}
.cmsg.ai .cmsg-bubble ol,.cmsg.ai .cmsg-bubble ul{padding-inline-start:16px}
.cmsg.ai .cmsg-bubble>*+*{margin-top:5px}
.cmsg.ai .cmsg-bubble li+li{margin-top:3px}
.cmsg.ai .cmsg-bubble p:empty{display:none}
.cmsg.ai .cmsg-bubble strong{color:var(--orange)}
.cmsg.ai .cmsg-bubble hr{display:none}
.chat-typing-dots{display:inline-flex;align-items:center;gap:4px;height:16px;padding:2px 0;direction:ltr}
.chat-typing-dots span{width:6px;height:6px;background:var(--muted);border-radius:50%;display:inline-block;animation:chatDotPulse 1.4s infinite ease-in-out both}
.chat-typing-dots span:nth-child(1){animation-delay:-0.32s}
.chat-typing-dots span:nth-child(2){animation-delay:-0.16s}
.chat-typing-dots span:nth-child(3){animation-delay:0s}
@keyframes chatDotPulse{0%,80%,100%{transform:scale(0.6);opacity:.35}40%{transform:scale(1);opacity:1}}

/* ── Alert history panel ─────────────────────────────────────────────────────── */
.bell-btn{position:relative;background:none;border:.5px solid var(--border);border-radius:10px;padding:9px 10px;cursor:pointer;line-height:1;-webkit-tap-highlight-color:transparent;color:var(--text);transition:all 0.2s ease}
.bell-btn:hover{background:var(--bg2);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.1)}
.bell-badge{position:absolute;top:-5px;right:-5px;min-width:16px;height:16px;background:var(--red);color:#fff;font-size:10px;font-weight:700;border-radius:8px;display:none;align-items:center;justify-content:center;padding:0 3px;line-height:1}
.bell-badge.show{display:flex}
.ah-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:170;opacity:0;pointer-events:none;transition:opacity .25s}
.ah-backdrop.open{opacity:1;pointer-events:all}
.ah-panel{position:fixed;top:0;right:0;bottom:0;width:min(300px,82vw);background:var(--bg);border-left:.5px solid var(--border);z-index:180;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s cubic-bezier(.32,.72,0,1)}
.ah-panel.open{transform:translateX(0)}
.ah-hdr{display:flex;align-items:center;justify-content:space-between;padding:max(16px, env(safe-area-inset-top)) 16px 12px;border-bottom:.5px solid var(--border);flex-shrink:0}
.ah-hdr-title{font-size:14px;font-weight:500}
.ah-close{background:none;border:.5px solid var(--border);border-radius:7px;color:var(--muted);font-size:18px;cursor:pointer;padding:1px 9px;line-height:1.4}
.ah-close:hover,.ah-close:active{background:var(--bg3)}
.ah-list{flex:1;overflow-y:auto;padding:4px 0 max(4px, env(safe-area-inset-bottom))}
.ah-empty{font-size:13px;color:var(--muted);text-align:center;padding:48px 20px}
.ah-item{padding:10px 16px;border-bottom:.5px solid var(--border)}
.ah-item:last-child{border-bottom:none}
.ah-item-time{font-size:10px;color:var(--muted);margin-bottom:4px}
.ah-item-msg{font-size:13px;color:var(--text);line-height:1.45}

/* ── Bottom nav bar ──────────────────────────────────────────────────────────── */
/* TODO #75: reported as floating mid-screen on mobile instead of pinning to
   the viewport bottom. The containing-block chain checked out clean live (no
   transform/filter/overflow ancestor breaks position:fixed) -- this is the
   known iOS Safari bug where a fixed element carrying backdrop-filter can
   detach from the viewport and scroll with content during momentum scroll.
   Forcing it onto its own compositing layer is the standard mitigation. */
.nav-bar{position:fixed;bottom:0;left:0;right:0;display:flex;background:color-mix(in srgb, var(--bg2) 85%, transparent);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-top:.5px solid var(--border);z-index:50;padding-bottom:env(safe-area-inset-bottom);padding-left:max(14px, env(safe-area-inset-left));padding-right:max(14px, env(safe-area-inset-right));box-shadow:0 -4px 20px rgba(0,0,0,0.2);transform:translateZ(0);-webkit-transform:translateZ(0);will-change:transform}
.nav-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:9px 4px 8px;background:none;border:none;color:var(--muted);font-size:10px;font-family:inherit;cursor:pointer;transition:color .2s;-webkit-tap-highlight-color:transparent;min-width:0}
.nav-item.active{color:var(--orange)}
.nav-item svg{width:22px;height:22px}
.nav-item>span{white-space:nowrap}

/* ── Tab panels ──────────────────────────────────────────────────────────────── */
.tab-panel{display:none}
.tab-panel.active{display:block}

/* ── Instructions ────────────────────────────────────────────────────────────── */
.ic{background:var(--bg2);border:.5px solid var(--border);border-radius:16px;padding:20px 24px;margin-bottom:12px;box-shadow:0 8px 32px rgba(0,0,0,0.1);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
.acc-item{border-top:.5px solid var(--border);margin-top:12px;padding-top:12px}
.acc-item:first-child{border-top:none;margin-top:0;padding-top:0}
.acc-header{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;min-height:44px;padding:4px 0;background:none;border:none;color:var(--text);font-size:15px;font-weight:500;font-family:inherit;text-align:start;cursor:pointer;-webkit-tap-highlight-color:transparent}
.acc-icon{width:18px;height:18px;flex-shrink:0;color:var(--muted);transition:transform .25s ease}
.acc-body-wrap{display:grid;grid-template-rows:0fr;transition:grid-template-rows .25s ease}
/* CHILD combinators, not descendant -- .acc-item nests (see .acc-sub below).
   As a descendant selector, an open OUTER item matched every nested wrap and
   icon too, so every sub-section would be forced open with no way to close it:
   the inner item's own :not(.open) state has no rule that outranks it. */
.acc-item.open > .acc-body-wrap{grid-template-rows:1fr}
.acc-item.open > .acc-header > .acc-icon{transform:rotate(90deg)}
.acc-body{overflow:hidden}
.acc-body p{margin:10px 0 0;font-size:13px;line-height:1.55;color:var(--muted)}
.acc-body p:first-child{margin-top:10px}
/* Step lists in the how-to sections. padding-inline-start (not padding-left)
   so the markers sit on the correct side under RTL Hebrew. */
/* Reference sub-sections nested inside a topic (the grate-calibration accordion
   keeps its steps open and folds setup/status/range/re-calibration/mistakes
   away). Sized down from the parent header so the hierarchy reads, but the
   44px tap floor from DESIGN-SYSTEM.md §4 is inherited, not overridden. */
.acc-sub{margin-top:8px;padding-top:8px}
.acc-sub > .acc-header{font-size:13px;font-weight:600}
.acc-sub > .acc-header > .acc-icon{width:16px;height:16px}
.acc-body ol,.acc-body ul{margin:8px 0 0;padding-inline-start:20px}
.acc-body li{margin-top:6px;font-size:13px;line-height:1.55;color:var(--muted)}

/* Header quick buttons hidden in all views — lang/theme/unit live in the Account popover */
.hdr-quick-btn{display:none}

/* ── Header anchors & triggers ─────────────────────────────────────────────── */
.account-anchor,.menu-anchor{position:relative;display:inline-flex;align-items:center}

/* User account & settings trigger (compact pill with avatar Letter and dropdown chevron) */
.user-pill-btn{display:inline-flex;align-items:center;justify-content:center;background:var(--bg2);border:.5px solid var(--border);border-radius:9999px;width:auto;height:36px;padding:3px 8px 3px 4px;gap:5px;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:all 0.2s ease;color:var(--text);font-family:inherit}
.user-pill-btn:hover{background:var(--bg3);border-color:color-mix(in srgb, var(--text) 25%, var(--border));transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.12)}
.user-pill-btn.open{background:var(--bg3);border-color:var(--orange)}
.user-avatar{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:color-mix(in srgb, var(--text) 18%, var(--bg3));color:var(--text);font-size:13px;font-weight:600;flex-shrink:0;overflow:hidden;text-transform:uppercase}
.user-avatar-img{width:100%;height:100%;object-fit:cover}
.user-name{display:none;font-size:13px;font-weight:500;color:var(--text);max-width:85px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.user-dot{display:none;color:var(--muted);font-size:12px;line-height:1}
.user-tier-label{display:none;font-size:12px;font-weight:600;letter-spacing:0.2px}
.user-tier-label.tier-pro{color:#f59e0b}
.user-tier-label.tier-vip{color:#a855f7}
.user-tier-label.tier-admin{color:#ec4899}
.user-tier-label.tier-free{color:var(--muted)}
.user-chevron{display:inline-block;color:var(--muted);transition:transform 0.2s ease;flex-shrink:0}
.user-pill-btn.open .user-chevron{transform:rotate(180deg)}

/* Hamburger menu button (header right) */
.menu-btn{display:flex;flex-direction:column;justify-content:center;gap:4px;align-items:center;background:none;border:.5px solid var(--border);border-radius:10px;padding:6px 7px;cursor:pointer;-webkit-tap-highlight-color:transparent;width:40px;height:40px;transition:all 0.2s ease}
.menu-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.1)}
.menu-btn.open{background:var(--bg3);border-color:var(--orange)}
.menu-btn span{display:block;width:15px;height:1.5px;background:var(--text);border-radius:1px;flex-shrink:0}

/* ── Popover dropdowns (Account & Smoker) ──────────────────────────────────── */
.account-drop,.menu-drop{display:none;position:absolute;top:calc(100% + 8px);background:color-mix(in srgb, var(--bg2) 95%, transparent);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:.5px solid var(--border);border-radius:16px;padding:8px 6px;z-index:200;box-shadow:0 12px 36px rgba(0,0,0,0.5),0 2px 8px rgba(0,0,0,0.25);animation:menuFadeIn 0.15s cubic-bezier(0.16,1,0.3,1)}
@keyframes menuFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
.account-drop{left:0;right:auto;min-width:260px;max-width:320px}
[dir="rtl"] .account-drop{left:auto;right:0}
.menu-drop{right:0;left:auto;min-width:220px;max-width:280px}
[dir="rtl"] .menu-drop{right:auto;left:0}
.account-drop.show,.menu-drop.show{display:block}

.menu-user-header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:background 0.15s ease}
.menu-user-header:hover{background:var(--bg3)}
.menu-user-header-left{display:flex;align-items:center;gap:10px;overflow:hidden}
.menu-user-avatar-lg{width:34px;height:34px;font-size:14px}
.menu-user-meta{display:flex;flex-direction:column;overflow:hidden}
.menu-user-name{font-size:13px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.menu-user-email{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.menu-section-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--muted);padding:6px 12px 2px}
.menu-drop-btn{display:flex;align-items:center;gap:10px;width:100%;padding:8px 12px;background:none;border:none;color:var(--text);font-size:13.5px;font-family:inherit;cursor:pointer;text-align:left;border-radius:8px;-webkit-tap-highlight-color:transparent;transition:background 0.15s ease}
[dir="rtl"] .menu-drop-btn{text-align:right}
.menu-drop-btn:hover,.menu-drop-btn:active{background:var(--bg3)}
.menu-drop-btn svg,.menu-drop-row svg{width:16px;height:16px;flex-shrink:0;color:var(--muted)}
.menu-drop-btn:hover svg{color:var(--text)}
.menu-drop-row{justify-content:space-between}
.menu-row-left{display:flex;align-items:center;gap:10px}
.menu-row-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;background:var(--bg3);color:var(--text);border:1px solid var(--border);font-variant-numeric:tabular-nums;line-height:1.4;width:78px;text-align:center;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.menu-row-badge.is-revealed{width:auto}
.menu-badge-accent{background:rgba(245,158,11,0.12);border-color:rgba(245,158,11,0.35);color:#f59e0b}
.menu-drop-btn.danger{color:var(--red)}
.menu-drop-btn.danger svg{color:var(--red)}
.unsupported-label{color:var(--red)}
.menu-drop-sep{height:.5px;background:var(--border);margin:4px 6px}

/* ── App bottom padding for nav bar ─────────────────────────────────────────── */
#app{padding-bottom:calc(76px + env(safe-area-inset-bottom));padding-top:max(1.5rem, env(safe-area-inset-top));padding-left:max(1.5rem, env(safe-area-inset-left));padding-right:max(1.5rem, env(safe-area-inset-right))}
@media (max-width: 480px) {
  .hdr{flex-wrap:nowrap;gap:6px}
  .hdrt{display:none}
  .hdrl{gap:4px;min-width:0}
  .hdr-brand{display:flex;align-items:center;gap:4px;flex-shrink:0}
  .user-name{max-width:60px}
  /* hdrl + hdrr need ~375px combined but only get ~294-324px at this
     breakpoint (measured 360-390px phones). hdrr's own flex-wrap:wrap
     (line 117) used to silently wrap the hamburger onto its own line inside
     hdrr once it ran out of room, inflating .hdr's height and centering
     hdrl in the middle of it -- reading as two misaligned rows. Dropping
     the "live"/"offline" status word and the wifi dBm number (the dot
     colour and wifi-bar colour already carry the same state, and the exact
     number is still in the account menu's WiFi row) claws back the ~62px
     needed to keep everything -- avatar, brand, both temps, bell,
     hamburger -- on a single row instead. */
  .hdrr{flex-wrap:nowrap;gap:6px;margin-inline-start:auto}
  #stxt{display:none}
  /* !important: js/dashboard.js's _renderWifiChip() sets this element's
     inline style.display directly on every RSSI update, which would
     otherwise win over a plain rule here regardless of specificity. */
  #wifi-val{display:none!important}
}

/* ── Chat bottom sheet ───────────────────────────────────────────────────────── */
.chat-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:150;opacity:0;pointer-events:none;transition:opacity .3s}
.chat-backdrop.open{opacity:1;pointer-events:all}
.chat-sheet{position:fixed;bottom:0;left:50%;width:100%;max-width:600px;height:52vh;background:var(--bg2);border-radius:18px 18px 0 0;border:.5px solid var(--border);border-bottom:none;z-index:160;display:flex;flex-direction:column;transform:translateX(-50%) translateY(100%);transition:transform .35s cubic-bezier(.32,.72,0,1)}
.chat-sheet.open{transform:translateX(-50%) translateY(0)}
.chat-handle{width:36px;height:4px;background:var(--border);border-radius:2px;margin:10px auto 6px;flex-shrink:0}
.chat-sheet-hdr{display:flex;align-items:center;gap:8px;padding:0 16px 10px;border-bottom:.5px solid var(--border);flex-shrink:0}
.chat-mini-temps{display:flex;gap:14px;flex:1}
.chat-mini-pit{font-size:14px;font-weight:600;color:var(--orange)}
.chat-mini-pit span,.chat-mini-meat span{font-size:10px;font-weight:400;color:var(--muted);letter-spacing:.04em;margin-right:3px}
.chat-mini-meat{font-size:14px;font-weight:600;color:#8899bb}
.chat-mini-fan{font-size:11px;color:var(--muted);align-self:center}
.chat-close-btn{background:none;border:.5px solid var(--border);border-radius:7px;color:var(--muted);font-size:18px;cursor:pointer;padding:1px 9px;line-height:1.4;flex-shrink:0}
.chat-close-btn:hover,.chat-close-btn:active{background:var(--bg3)}
.chat-sheet-body{flex:1;display:flex;flex-direction:column;padding:12px 16px max(12px, env(safe-area-inset-bottom));overflow:hidden}
.chat-sheet-body #chat-msgs{flex:1;height:auto;margin-bottom:10px}

/* ── RTL (Hebrew) ────────────────────────────────────────────────────────────
   Flexbox rows (headers, nav bar, alert rows, menu items, tabs) already
   mirror automatically once html[dir="rtl"] is set (browser default) --
   these overrides only cover elements positioned with explicit physical
   left/right/text-align that don't auto-flip: absolute-positioned overlays
   anchored to a screen edge, and a couple of asymmetric visual details
   (toggle-switch knob, chat-bubble tail corner) that should mirror to feel
   native rather than just "translated but still LTR-shaped". */
html[dir="rtl"] #bnr{right:auto;left:max(1.5rem, env(safe-area-inset-left));text-align:right}
html[dir="rtl"] .menu-drop{right:auto;left:0}
html[dir="rtl"] .menu-drop-btn{text-align:right}
html[dir="rtl"] .ah-panel{right:auto;left:0;border-left:none;border-right:.5px solid var(--border);transform:translateX(-100%)}
html[dir="rtl"] .ah-panel.open{transform:translateX(0)}
html[dir="rtl"] .bell-badge{right:auto;left:-5px}
/* The session-list chevron is a directional glyph, not decoration: in RTL it
   must point the way the list actually opens. Its spacing already uses
   margin-inline-start (history.js), so only the glyph needs flipping. */
html[dir="rtl"] .hist-chevron{transform:scaleX(-1)}
html[dir="rtl"] .tog-sl::before{left:auto;right:3px}
html[dir="rtl"] .tog input:checked + .tog-sl::before{transform:translateX(-20px)}
html[dir="rtl"] .cmsg.user .cmsg-bubble{border-bottom-right-radius:12px;border-bottom-left-radius:3px}
/* Directional "back" arrows -- matches this app's existing convention of
   flipping arrow direction for RTL reading order (the old text-embedded
   arrow characters did this per-string; real SVG icons need an explicit
   mirror instead). */
html[dir="rtl"] .rtl-flip-icon{transform:scaleX(-1)}
html[dir="rtl"] .cmsg.ai .cmsg-bubble{border-bottom-left-radius:12px;border-bottom-right-radius:3px}

/* Indeterminate OTA progress. The device cannot report a real percentage
   during a download: pushing progress needs a second mbedTLS context (~36KB
   of internal SRAM, two 16KB TLS record buffers plus session state) alongside
   the one the download itself is holding, and that allocation has already
   crashed a device mid-flash. So ota_manager.cpp deliberately pushes only
   "downloading" at the start and "rebooting"/"failed" at the end. A bar that
   sits at 0% and then snaps to 100% reads as broken; a sweep is honest about
   the fact that the duration genuinely isn't known. */
#dash-ota-prog-bar.indeterminate{
  width:35% !important;
  transition:none;
  animation:ota-sweep 1.1s ease-in-out infinite;
}
@keyframes ota-sweep{
  0%   {margin-inline-start:-35%}
  100% {margin-inline-start:100%}
}
/* Honour a reduced-motion preference: fall back to a static part-filled bar
   rather than a sweeping one. Still not a real percentage -- just not moving. */
@media (prefers-reduced-motion: reduce){
  #dash-ota-prog-bar.indeterminate{animation:none;margin-inline-start:0}
  .card.card-slot-spin{animation:none!important}
  .slot-digit.roll-up .slot-char,.slot-digit.roll-down .slot-char{animation:none!important}
  .slot-digit.roll-up::before,.slot-digit.roll-down::before{display:none!important}
}

/* Subscription Tier Badges */
.tier-pill {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.5px;
  padding: 1px 6px;
  border-radius: 4px;
  text-transform: uppercase;
  vertical-align: middle;
  line-height: 1.4;
}
.tier-pill.tier-pro {
  background: linear-gradient(135deg, rgba(245, 166, 35, 0.2), rgba(224, 90, 34, 0.2));
  color: #f5a623;
  border: 1px solid rgba(245, 166, 35, 0.4);
}
.tier-pill.tier-admin {
  background: rgba(155, 89, 182, 0.2);
  color: #bb86fc;
  border: 1px solid rgba(155, 89, 182, 0.4);
}
.tier-pill.tier-vip {
  background: rgba(236, 72, 153, 0.2);
  color: #ec4899;
  border: 1px solid rgba(236, 72, 153, 0.4);
}
.tier-pill.tier-free {
  background: rgba(255, 255, 255, 0.08);
  color: var(--muted);
  border: 1px solid var(--border);
}

/* Subscription Modal UI */
.sub-status-card {
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
}

.sub-features-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: rgba(0, 0, 0, 0.15);
  border-radius: 10px;
  padding: 12px 14px;
  border: 1px solid var(--border);
}

.sub-feat-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text);
}

.sub-feat-item svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--orange);
}

.sub-payment-box {
  background: rgba(0, 0, 0, 0.2);
  border-radius: 10px;
  padding: 12px 14px;
  border: 1px dashed var(--border);
}

.sub-btn-upgrade {
  background: linear-gradient(135deg, #f5a623, #e05a22);
  color: #fff;
  border: none;
  font-weight: 600;
  font-size: 14px;
  padding: 12px 16px;
  border-radius: 8px;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(224, 90, 34, 0.35);
  transition: transform 0.15s, filter 0.15s;
}
.sub-btn-upgrade:hover {
  filter: brightness(1.08);
}
.sub-btn-upgrade:active {
  transform: scale(0.98);
}

.sub-btn-downgrade {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  font-size: 12px;
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.sub-btn-downgrade:hover {
  color: #ff5252;
  border-color: rgba(255, 82, 82, 0.4);
}

.sub-plan-selector {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  background: rgba(255, 255, 255, 0.04);
  padding: 4px;
  border-radius: 10px;
  border: 1px solid var(--border);
}

.sub-plan-pill {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 8px 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  color: var(--muted);
  transition: all 0.2s ease;
  font-family: inherit;
}

.sub-plan-pill:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.03);
}

.sub-plan-pill.active {
  background: rgba(245, 166, 35, 0.12);
  border-color: rgba(245, 166, 35, 0.45);
  color: var(--text);
}

.sub-plan-name {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.sub-plan-price {
  font-size: 13px;
  font-weight: 700;
  color: #f5a623;
}

.sub-save-badge {
  font-size: 10px;
  font-weight: 700;
  color: #10b981;
  background: rgba(16, 185, 129, 0.15);
  border: 1px solid rgba(16, 185, 129, 0.3);
  border-radius: 4px;
  padding: 1px 4px;
  letter-spacing: 0.3px;
  white-space: nowrap;
}


