* { box-sizing: border-box; }

:root {
  --bg: #ede7d8;
  --card-bg: #fdfbf5;
  --border: #ddd5bf;
  --navy: #1c3550;
  --navy-soft: #4a5b6e;
  --label: #8f8874;
  --green: #1f8a5a;
  --gold: #b8862c;
  --gold-bg: #f5e8cf;
  --red: #cf4a4a;
  --red-bg: #fbe6e6;
  --blue: #2f6f9e;
  --teal-bg: #dcf0e8;

  /* SVG gauge colors (compass, RPM tach, anchor radar) - kept separate from
     the palette above so gauges can be re-themed without touching JS. */
  --svg-track: #c9d4dd;
  --svg-tick: #9a9280;
  --svg-label: #1c3550;
  --svg-label-dim: #8f8874;
  --svg-accent: #b8862c;
  --svg-bow: #2f6f9e;
  --svg-alarm: #cf4a4a;
  --svg-face: #ddd7c6;
  --svg-halo: #ffffff; /* halo behind ring/ticks/text so they stay readable over the chart */

  /* Weather icon colors (sun/cloud/rain glyphs) */
  --icon-sun: #f0a83a;
  --icon-cloud: #e8ebef;
  --icon-cloud-outline: #b7bec8;
  --icon-rain: #4a90c2;
  --icon-bolt: #e0a83a;

  /* Dark page scheme (independent of day/night - see body.dark-scheme
     below), plus the engine-cluster/rudder/hull-fouling gauge tiles which
     always use this same dark styling regardless of page. Deliberately
     separate from --navy/--navy-soft: those flip color on the day<->night
     boundary in a way that would be wrong here, since a dark-scheme page's
     background never changes with day/night - only night mode's red tint
     should reach this text, not the day/night background swap the rest of
     the app does. */
  --eng-text: #e8e8e8;
  --eng-text-dim: #9aa1ab;

  /* The Media tile's faceplate is always dark (a real head unit's casing
     doesn't change with day/night), but its backlit LCD text still owes the
     same night-vision rule everything else on the boat follows - amber by
     day, red at night. Kept separate from --eng-text since the LCD's day
     color is deliberately amber, not the neutral off-white Engine uses. */
  --media-lcd: #22e0a3;
  --media-lcd-dim: #1c8f6c;
  --media-lcd-glow: rgba(34, 224, 163, 0.35);
  --media-lcd-glow-soft: rgba(34, 224, 163, 0.16);

  /* Wind gauge tile - a dark instrument-screen look regardless of day/
     night (like Media's LCD faceplate), so most of these tokens don't
     change here vs the night block below. Only the apparent/true needle
     colors follow the night-vision rule (real color by day, red-only at
     night) - port/starboard band colors stay fixed literals instead, same
     rationale as the rudder gauge's port/starboard (see its own comment):
     which side is safety-relevant enough that night mode shouldn't
     collapse it toward a single hue. */
  --wind-bezel: #17181b;
  --wind-bezel-edge: #2c2e33;
  --wind-screen: #050607;
  --wind-white: #f4f6f8;
  --wind-dim: #8a9099;
  --wind-ring: #4a4e57;
  --wind-apparent: #2e8fff;
  --wind-true: #ffb400;
}

:root[data-theme="night"] {
  --bg: #000000;
  --card-bg: #150000;
  --border: #4a0000;
  --navy: #ff3232;
  --navy-soft: #b33a3a;
  --label: #7a2e2e;
  --green: #ff3232;
  --gold: #ff5a2e;
  --gold-bg: #2a0808;
  --red: #ff0000;
  --red-bg: #3a0000;
  --blue: #ff3232;
  --teal-bg: #2a0808;

  --svg-track: #5a1414;
  --svg-tick: #8a2020;
  --svg-label: #ff3232;
  --svg-label-dim: #a03030;
  --svg-accent: #ff5a2e;
  --svg-bow: #ff3232;
  --svg-alarm: #ffffff;
  --svg-face: #0d0000;
  --svg-halo: #000000;

  --eng-text: #ff3232;
  --eng-text-dim: #b33a3a;

  --media-lcd: #ff3232;
  --media-lcd-dim: #b33a3a;
  --media-lcd-glow: rgba(255, 50, 50, 0.35);
  --media-lcd-glow-soft: rgba(255, 50, 50, 0.16);

  --wind-apparent: #ff5a2e;
  --wind-true: #ff3232;

  --icon-sun: #ff5a2e;
  --icon-cloud: #3a1414;
  --icon-cloud-outline: #7a2e2e;
  --icon-rain: #ff3232;
  --icon-bolt: #ffffff;
}

/* Per-page dark scheme (right-click a page's tab in the bottom strip) -
   overrides the SAME palette tokens night mode does, but toward a neutral
   dark grey instead of red, since this is a background/look choice, not a
   night-vision one. Layered UNDER :root[data-theme="night"] on purpose:
   --navy/--navy-soft below point at --eng-text/--eng-text-dim rather than
   hardcoded greys, so a dark-scheme page that also has night mode on
   automatically gets red text too, without a third "dark-scheme AND night"
   variant to keep in sync by hand - the cascade does it. Semantic/status
   colors (--green/--gold/--red/--blue/--svg-accent/--svg-alarm) are
   deliberately left alone here, same reasoning as the Engine gauge cluster:
   a status color IS the information, dark background or not. */
body.dark-scheme {
  --bg: #0b0d10;
  --card-bg: #1c2127;
  --border: #2b3038;
  --navy: var(--eng-text);
  --navy-soft: var(--eng-text-dim);
  --label: var(--eng-text-dim);
  --gold-bg: #3a2f14;
  --red-bg: #3a1414;
  --teal-bg: #142a24;

  --svg-track: #343b44;
  --svg-tick: #4a5158;
  --svg-label: var(--eng-text);
  --svg-label-dim: var(--eng-text-dim);
  --svg-face: #1c2127;
  --svg-halo: #0b0d10;

  --icon-cloud: #2b3038;
  --icon-cloud-outline: #4a5158;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background: var(--bg);
  color: var(--navy);
  font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
  /* Auto rather than hidden, purely as a defensive fallback - #viewport-root
     below is sized/scaled to always fit the real viewport exactly, so
     scrolling shouldn't normally be needed at all. */
  overflow: auto;
  user-select: none;
}
body {
  /* Centers #viewport-root when its scaled aspect ratio doesn't exactly
     match the device's (e.g. a portrait phone showing a wide kiosk layout
     scaled down to fit its width) - the leftover space becomes an even
     margin on both sides/top-bottom instead of the content sitting pinned
     in a corner. */
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Everything the app actually draws (header, tiles, tab strip) scales as one
   unit to fit whatever screen it's opened on - a phone, tablet, laptop
   browser, or the kiosk's own monitor. Its unzoomed width/height are set
   directly (in JS, via applyViewportScale()) to the reference "design"
   screen size the layout was tuned against - almost always whatever device
   first ever loaded the dashboard - and its `zoom` is set to
   min(viewport/design) on load and on every resize/orientation change, so
   the whole layout shrinks or grows together rather than any one tile being
   clipped or landing off-screen on a differently-sized or differently-
   shaped display. The Settings drawer is deliberately NOT inside this
   wrapper, so it always stays at native, readable size regardless of how
   small the scaled dashboard itself is on a given device. */
#viewport-root {
  flex: none;
}

/* ---------- Header ---------- */
#topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  /* Extra bottom padding reserves dedicated room for #ip-badge below -
     without it, #ip-badge's `bottom: 3px` (measured from #topbar's own
     padding-box) lands inside the same vertical space align-items:center
     already centers #boat-name-block's two lines into, and its "DASHBOARD"
     subtitle collides with the IP text. Confirmed directly on a real phone
     in landscape (wide enough that #ip-badge isn't hidden by the narrow-
     screen rule below, so the underlying collision was fully visible). */
  /* Installed on an iPhone the app draws edge to edge (viewport-fit=cover +
     a translucent status bar), so the header keeps the status bar's height
     clear above its own content; env() is 0 everywhere else. */
  padding: calc(10px + env(safe-area-inset-top, 0px)) 20px 24px;
  background: var(--card-bg);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 10;
}

#boat-name-block { display: flex; flex-direction: column; line-height: 1.15; }
#boat-name {
  font-size: 1.3rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  color: var(--navy);
  cursor: pointer;
}
.boat-name-input {
  font: inherit;
  font-weight: 800;
  letter-spacing: 0.03em;
  color: var(--navy);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0 4px;
  width: 12ch;
}
#screen-title {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--label);
}

#position-badge {
  font-size: 1.7rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  color: var(--navy);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  margin-left: 40px;
}
/* Pulled out of the header's flex row and pinned to its bottom edge,
   centered - it's low-priority info (how to reach the box over SSH/LAN),
   so it shouldn't compete for space with the boat name, alarms, or clock. */
#ip-badge {
  position: absolute;
  left: 50%;
  bottom: 3px;
  transform: translateX(-50%);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--label);
  white-space: nowrap;
}

#alarm-banner {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-width: 0;
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--red);
  opacity: 0;
  visibility: hidden;
}
#alarm-banner.visible {
  opacity: 1;
  visibility: visible;
  animation: blink 1s step-start infinite;
}
#alarm-banner-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#alarm-silence-btn {
  flex-shrink: 0;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--red);
  background: var(--red-bg);
  border: 1px solid var(--red);
  border-radius: 6px;
  padding: 4px 8px;
  cursor: pointer;
  animation: none;
}
#alarm-silence-btn.silenced {
  color: var(--label);
  background: var(--card-bg);
  border-color: var(--border);
}
#alarm-clear-btn {
  flex-shrink: 0;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--red);
  background: var(--red-bg);
  border: 1px solid var(--red);
  border-radius: 6px;
  padding: 4px 8px;
  cursor: pointer;
  animation: none;
}

#live-indicator {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--green);
}
#live-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--green);
}
#live-indicator.offline { color: var(--label); }
#live-indicator.offline #live-dot { background: var(--label); }
/* Fed by summary polling rather than the live stream - see the Data source
   section in app.js. Gold, not grey: the numbers are real and recent, just
   not streaming. */
#live-indicator.remote { color: var(--gold); }
#live-indicator.remote #live-dot { background: var(--gold); }

/* "Open on your phone" QR in Settings > System (loadPhoneQr). The canvas is
   drawn at 6px per module and shown at a fixed size; pixelated keeps the
   modules crisp instead of blurring them when that isn't a 1:1 mapping. */
.phone-qr-row { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.phone-qr { width: 180px; height: 180px; image-rendering: pixelated; border-radius: 8px; background: #fff; flex: 0 0 auto; }
.phone-qr-text { flex: 1 1 200px; min-width: 0; }
.phone-qr-url { font-weight: 700; font-size: 0.85rem; word-break: break-all; margin-bottom: 6px; }

/* Camera tile through a relay: a still frame + watch-on-request, then a
   countdown over the live video (renderRemoteCameraStill & co). */
.cam-remote-still { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.cam-remote-img { max-width: 100%; min-width: 220px; min-height: 120px; border-radius: 8px; background: #000; object-fit: cover; }
.cam-remote-unavailable { padding: 30px 12px; color: var(--label); font-weight: 700; font-size: 0.85rem; }
.cam-remote-note { font-size: 0.7rem; color: var(--label); text-align: center; max-width: 280px; }
.cam-remote-countdown {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  background: rgba(0, 0, 0, 0.65); color: #fff; border: 0; border-radius: 6px;
  padding: 4px 8px; font-size: 0.75rem; font-weight: 700; cursor: pointer; font-variant-numeric: tabular-nums;
}

/* Sign-in form a relay shows before the dashboard (showRelayLogin). */
#relay-login {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg); padding: 20px;
}
#relay-login form {
  width: 100%; max-width: 360px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 14px;
  padding: 26px 24px; display: flex; flex-direction: column; gap: 12px;
}
.relay-login-title { font-size: 1.4rem; font-weight: 800; color: var(--navy); }
.relay-login-sub { color: var(--label); font-size: 0.85rem; line-height: 1.4; margin-bottom: 4px; }
#relay-login label { display: flex; flex-direction: column; gap: 4px; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--label); }
#relay-login input { font-size: 1.05rem; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--navy); }
#relay-login button { margin-top: 6px; font-size: 1rem; }
.relay-login-error { min-height: 1.2em; color: var(--red); font-size: 0.85rem; font-weight: 700; }

/* Boat Status tile - the remote check-in card (renderBoatStatusPanel). */
.status-list { display: flex; flex-direction: column; gap: 6px; min-width: 250px; }
.status-row { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; }
.status-label { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--label); white-space: nowrap; }
.status-value { font-size: 0.95rem; font-weight: 800; color: var(--navy); text-align: right; font-variant-numeric: tabular-nums; }
.status-value.status-good, .status-footer.status-good { color: var(--green); }
.status-value.status-bad, .status-footer.status-bad { color: var(--red); }
.status-value.status-warn, .status-footer.status-warn { color: var(--gold); }
.status-footer { margin-top: 4px; font-size: 0.7rem; font-weight: 700; color: var(--label); text-align: right; }

#clock-block {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: 14px;
}
#clock-date {
  font-size: 1.9rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--navy);
  white-space: nowrap;
}
#clock-time {
  font-variant-numeric: tabular-nums;
  color: var(--gold);
  font-size: 1.9rem;
  font-weight: 800;
  white-space: nowrap;
}

#gear-btn, #theme-toggle-btn {
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--navy);
  font-size: 1.2rem;
  border-radius: 8px;
  width: 40px;
  height: 40px;
  cursor: pointer;
}
#gear-btn:active, #theme-toggle-btn:active { background: var(--border); }

#done-editing-btn {
  display: none;
  background: var(--gold);
  border: none;
  color: #fff;
  font-size: 0.85rem;
  font-weight: 700;
  padding: 0 16px;
  height: 40px;
  border-radius: 8px;
  cursor: pointer;
  white-space: nowrap;
}
#done-editing-btn:active { opacity: 0.85; }
body.layout-editing #done-editing-btn { display: block; }

/* Always-available way to reach the add-tile menu during layout editing,
   instead of it only being reachable by finding a patch of genuinely
   empty canvas to right-click/long-press on - confirmed as a real problem
   on a phone specifically: removing tiles shrinks the canvas down to fit
   what's left, so there's often barely any empty space left to find at
   all. Opens the exact same menu (openAddTileMenu) as right-click/long-
   press on empty canvas, just from a fixed, always-reachable spot. */
#add-tile-btn {
  display: none;
  background: #2c8f52;
  border: none;
  color: #fff;
  font-size: 0.85rem;
  font-weight: 700;
  padding: 0 16px;
  height: 40px;
  border-radius: 8px;
  cursor: pointer;
  white-space: nowrap;
}
#add-tile-btn:active { opacity: 0.85; }
body.layout-editing #add-tile-btn { display: block; }

/* The header row was built for a wide kiosk/desktop screen - boat name +
   coordinates + IP info + clock + buttons all fit on one un-wrapping line
   there. Confirmed directly on a phone: #position-badge (1.7rem font, 40px
   left margin, nowrap) and #clock-block (1.9rem, nowrap) alone need far
   more width than a phone's own designViewport actually has, so they
   overflow/clip the screen edge regardless of the overall canvas scale
   being correct for that device - this is a separate problem from the
   scale factor itself (see applyViewportScale in app.js), which only
   controls how big a correctly-laid-out header would be, not whether this
   one reflows to fit a narrow row at all. */
@media (max-width: 700px) {
  #topbar {
    flex-wrap: wrap;
    gap: 8px 12px;
    padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px;
  }
  #position-badge {
    font-size: 0.95rem;
    margin-left: 0;
  }
  #clock-block { gap: 8px; }
  #clock-date, #clock-time {
    font-size: 1rem;
  }
  /* Already the lowest-priority thing in the header by its own design (see
     its comment above - "shouldn't compete for space") - three network
     addresses just don't fit a phone-width row at any reasonable size, and
     it's not information a phone user typically needs (it's for reaching
     THIS dashboard FROM another device, most useful read from a laptop
     mid-setup, not from the phone already looking at it). Dropped
     entirely on a narrow screen rather than shrunk to illegibility.
     Nothing else needs it - it's purely informational, no interaction. */
  #ip-badge {
    display: none;
  }
  /* A phone shows its own date in the status bar, and every row the header
     wraps onto is a row the Boat Status card can't have - on a 375px-wide
     screen the full header ran to four rows and ~40% of the height before
     this. Two rows now: name + position, then indicator + time + buttons. */
  #clock-date { display: none; }
  #clock-time { font-size: 0.95rem; }
  #theme-toggle-btn, #gear-btn { padding: 4px 9px; font-size: 0.95rem; }
  /* The original dashboard's three side-by-side zones can't share a phone's
     width - flow them one after another instead. Freeform (arranged)
     layouts are untouched: they're display:block already. */
  .columns:not(.columns-freeform) { grid-template-columns: 1fr; }
}

/* ---------- Main layout ---------- */
#app {
  padding: 14px 16px calc(60px + env(safe-area-inset-bottom, 0px));
}

.view { display: none; }
.view.active { display: block; }

.columns {
  display: grid;
  grid-template-columns: 0.6fr 1.9fr 1fr;
  gap: 12px;
  align-items: start;
}

.column {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px;
}
/* One flowing zone (any page but the original dashboard, before a device
   has arranged it) takes the full width rather than the first grid track. */
.columns.columns-single { display: block; }
.columns.columns-single .column { width: 100%; }

/* Free-form mode: exact pixel placement instead of the 3-zone grid flow. */
.columns.columns-freeform {
  display: block;
  position: relative;
  /* Safety net, not the primary defense - app.js now clamps every tile's
     drag/resize/auto-placed position so its own corner can never land
     outside this canvas (see the resize/drag pointermove handlers and
     layoutPanelsFreeform in app.js). But a card is absolutely positioned
     inside this box, so if one ever DOES end up wider than the canvas
     anyway (a future bug, a hand-edited settings.json, a page migrated in
     from somewhere else), its bleed is real overflow of THIS element, not
     just a visual illusion - give it its own scrollbar right here so it's
     always reachable by scrolling sideways, instead of depending on
     whatever body/#viewport-root's zoom + flex-centering happens to do
     with overflow further up the tree. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
body.layout-editing .columns.columns-freeform {
  background-image:
    linear-gradient(to right, var(--border) 1px, transparent 1px),
    linear-gradient(to bottom, var(--border) 1px, transparent 1px);
  background-size: 20px 20px;
  border-radius: 8px;
}

.panel {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 10px 12px;
  transition: border-color 0.2s, background 0.2s;
  /* Double-tap anywhere on a tile maximizes it (see buildPanelShell's
     card-level handler). `manipulation` keeps normal panning/scrolling and
     pinch-zoom but takes DOUBLE-TAP away from the browser's own zoom
     gesture - which on iOS Safari would otherwise claim it (and add a ~300ms
     delay to single taps). Without this the gesture doesn't work from an
     iPhone. Applies to the whole tile now, not just the title bar, since a
     double-tap anywhere on the tile triggers it. */
  touch-action: manipulation;
}
.panel-compact {
  align-self: center;
  width: fit-content;
}

/* Chromium-only kiosk, so `zoom` (non-standard but rock-solid in Blink, and
   supported in modern Safari/Chrome on phones/tablets too) is a cheap way to
   scale a whole tile - text, icons, SVG gauges, padding - and have it
   actually affect layout flow, without every render function needing its
   own size-aware logic. Works uniformly for any panel type, present or
   future. Tile size is now a continuous drag-to-resize value (see
   applyPanelSizing() in app.js) rather than 3 fixed classes - it sets this
   zoom directly via inline style, computed from wherever you drag the
   tile's corner handle to.
   It lives on the .panel-zoom wrapper INSIDE the card, not the card itself -
   zoom on the card would also rescale the card's own left/top offsets
   (zoom changes the pixel scale an element resolves its own box properties
   against), desyncing the saved layout position from where the tile
   actually renders as soon as its size changes. */

/* ---------- Layout edit mode ---------- */
/* Floats over the (dimmed) card content instead of sitting in normal flow -
   if it pushed the card taller like a normal toolbar would, tiles lined up
   edge-to-edge in edit mode would leave a gap once it disappears on Done,
   since the card shrinks back down but sibling tiles' saved positions don't
   move. Overlaying it keeps card height identical in and out of edit mode. */
.tile-edit-bar {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--gold-bg);
  border: 1px solid var(--gold);
  border-radius: 8px;
  padding: 4px 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.tile-drag-handle {
  width: 30px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 1rem;
  color: var(--navy);
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.tile-drag-handle:active { cursor: grabbing; background: var(--border); }

/* Continuous resize handle - pinned to the tile's visual bottom-right
   corner (not inside the toolbar, so it sits where a resize grip is
   expected on any OS window), dragged to scale the whole tile smoothly
   instead of jumping between fixed S/M/L steps. */
.tile-resize-grip {
  position: absolute;
  bottom: -4px;
  right: -4px;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 2px 3px;
  font-size: 1rem;
  line-height: 1;
  color: var(--gold);
  background: var(--gold-bg);
  border: 1px solid var(--gold);
  border-radius: 8px;
  cursor: nwse-resize;
  touch-action: none;
  user-select: none;
  z-index: 5;
}
.tile-resize-grip:active { background: var(--border); }

.layout-editing .panel { outline: 2px dashed var(--border); outline-offset: 3px; }
.layout-editing .panel-body { opacity: 0.45; pointer-events: none; }
/* Deliberate exception to the pointer-events:none above - lets a reading be
   picked up and dragged straight off its still-attached parent tile while
   editing, without needing to detach it via Setup first. */
.layout-editing .panel-body [data-field] { pointer-events: auto; cursor: grab; }
.panel.tile-dragging {
  z-index: 50;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
  opacity: 0.92;
}

/* A custom group's member rows - each wraps whatever markup that reading
   normally renders as (a plain digital block, or a cloned field fragment),
   plus an edit-mode-only "remove from group" control. */
.group-member-wrap { position: relative; }
.group-member-remove {
  display: none;
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: none;
  background: var(--red);
  color: #fff;
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
  z-index: 10;
  align-items: center;
  justify-content: center;
  /* .layout-editing .panel-body disables pointer-events wholesale (editing
     is about dragging/resizing the card, not its content) - this control is
     the one deliberate exception. */
  pointer-events: auto;
}
.layout-editing .group-member-remove { display: flex; }

/* A custom group's members lay out as a grid with an explicit column count
   (--group-cols, set inline per group - see groupColumnsFor/cycleGroupColumns
   in app.js and the ⊞ toolbar button), not one derived from the tile's own
   size - width and zoom scale together for every tile including this one,
   which cancels out if column count also tried to key off width. The
   compound selector (not just .group-members-row) is deliberate - this
   element also carries .eng-digital-row for the shared margin-top, which is
   defined later in this file and would otherwise win the display/
   grid-template-columns properties on source order alone despite equal
   specificity. */
.eng-digital-row.group-members-row {
  display: grid;
  grid-template-columns: repeat(var(--group-cols, 2), minmax(90px, 1fr));
}

.tile-columns-btn {
  border: none;
  background: transparent;
  color: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 4px;
}
.tile-columns-btn:active { background: var(--border); }

/* Remove-this-tile, straight from the canvas edit toolbar - red like every
   other destructive/removal control in this app (.group-member-remove,
   the Setup delete buttons), not gold like the drag handle/columns button
   next to it, since it's a meaningfully different kind of action. */
.tile-remove-btn {
  border: none;
  background: transparent;
  color: var(--red);
  font-size: 0.75rem;
  font-weight: 700;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 4px;
  line-height: 1;
}
.tile-remove-btn:active { background: var(--red-bg); }

/* Right-click (or long-press) on empty canvas while editing - the "add a
   tile to this page" picker. Fixed-position, positioned inline via JS at
   the click point (see openAddTileMenu in app.js). */
.add-tile-menu {
  position: fixed;
  z-index: 80;
  min-width: 180px;
  max-width: 220px;
  max-height: 320px;
  overflow-y: auto;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.add-tile-menu-item {
  border: none;
  background: transparent;
  color: var(--navy);
  font-size: 0.8rem;
  font-weight: 600;
  text-align: left;
  padding: 8px 10px;
  border-radius: 6px;
  cursor: pointer;
}
.add-tile-menu-item:active { background: var(--gold-bg); }
.add-tile-menu-empty {
  font-size: 0.75rem;
  color: var(--label);
  padding: 8px 10px;
  text-align: center;
}

/* Live highlight on whichever tile a dragged reading is currently overlapping
   enough to combine with, if released here - the drag-to-merge gesture. */
.panel.tile-merge-target {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px rgba(184, 134, 44, 0.18);
}

.align-guide-v, .align-guide-h {
  position: absolute;
  background: var(--red);
  pointer-events: none;
  z-index: 60;
  display: none;
}
.align-guide-v { top: 0; bottom: 0; width: 2px; }
.align-guide-h { left: 0; right: 0; height: 2px; }

.panel-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--navy);
  margin-bottom: 8px;
}
.panel-title::after {
  content: '';
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.3;
}
.panel-title.title-teal { color: var(--green); }
.panel-title.title-gold { color: var(--gold); }

.panel-title-collapsible { cursor: pointer; user-select: none; }
.panel-title-collapsible::after { display: none; }
.panel-collapse-chevron { margin-left: auto; display: inline-block; transition: transform 0.15s ease; font-size: 0.9em; }
.panel-collapsed .panel-collapse-chevron { transform: rotate(-90deg); }
.panel-collapsed .panel-body { display: none; }
.panel-collapsed .panel-title { margin-bottom: 0; }

/* A hidden-or-detached field inside a multi-field tile - the markup still
   exists (unlike the old omit-from-template approach) so a detached field
   has something for its own mini-tile to clone the current value from. */
/* !important is deliberate here, not just belt-and-suspenders: this class
   is always applied ALONGSIDE some other, tile-specific class (awa-row,
   tank-row, eng-digital-block, ...), and several of those set their own
   `display` too - at equal specificity (one class each), whichever rule
   comes LATER in this file wins, which silently defeated this exact class
   for .awa-row (defined after this) even though it worked fine for
   classes defined earlier. This needs to win regardless of source order,
   since new tile-specific classes get added over time without anyone
   thinking to check where they land relative to this rule. */
.field-in-parent-hidden { display: none !important; }
/* Same field, shown while editing the layout instead of display:none -
   deliberately doesn't touch `display` at all (that's exactly what makes
   it safe: the element keeps whatever display type its own tile's markup
   needs - flex row item, table row, whatever - instead of a separate
   override class fighting that tile's own CSS rule for the same
   property). Right-click it to bring it back - see wireFieldContextMenu. */
.field-hidden-preview {
  opacity: 0.35;
  outline: 1px dashed var(--border);
  outline-offset: -2px;
  cursor: pointer;
}

.panel.no-data { opacity: 0.55; }
/* The tile itself no longer shrinks to fit the much shorter "no data"
   placeholder (see panelMinSize/applyMinSize in app.js) - center that
   placeholder within the resulting extra space instead of leaving it
   stranded at the top of a now-taller box. Making the card a flex column
   lets .panel-body (flex:1) actually grow to fill that space, so its own
   content has somewhere to center within. */
.panel.no-data {
  display: flex;
  flex-direction: column;
}
.panel.no-data .panel-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.panel.alarm {
  border-color: var(--red);
  background: var(--red-bg);
  animation: blink-border 1s step-start infinite;
}

/* ---------- Metric panel ---------- */
.metric-value {
  font-size: 2rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
  color: var(--navy);
}
.metric-unit {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--navy-soft);
  margin-left: 4px;
}
.metric-sublabel {
  font-size: 0.72rem;
  color: var(--label);
  margin-top: 2px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.panel.no-data .metric-sublabel { color: var(--red); font-weight: 700; }

/* A plain metric tile (Depth/Sea Temp) as a member of a custom group - same
   light-cream card language as everywhere else in this theme. Deliberately
   NOT .eng-digital-block, which is the dark instrument-panel style used for
   Speed/Odometer/Attitude/engine gauges - reusing that here made a metric
   reading (light everywhere else it appears) turn solid black/white the
   moment it joined a group. */
.group-metric-block {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 16px;
  text-align: center;
  min-width: 100px;
}
.group-metric-label { font-size: 0.62rem; font-weight: 700; color: var(--label); text-transform: uppercase; letter-spacing: 0.04em; }
.group-metric-value { font-size: 1.4rem; font-weight: 800; color: var(--navy); font-variant-numeric: tabular-nums; }
.group-metric-unit { font-size: 0.65rem; color: var(--navy-soft); margin-left: 3px; font-weight: 600; }

.metric-total {
  font-size: 0.75rem;
  color: var(--navy-soft);
  margin-top: 4px;
}

/* ---------- Navigation-to-waypoint panel ---------- */
/* Same digital-block idea as the engine screen's eng-digital-* (a small
   labeled chip per number), but built on the day/night theme vars instead
   of hardcoded dark colors - this panel lives on the light dashboard
   screen, not the always-dark engine screen. */
.nav-digital-row { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.nav-digital-row + .nav-digital-row { margin-top: 10px; }
.nav-digital-block {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 14px;
  text-align: center;
  min-width: 90px;
}
.nav-digital-label { font-size: 0.62rem; font-weight: 700; color: var(--label); text-transform: uppercase; letter-spacing: 0.04em; }
.nav-digital-value { font-size: 1.3rem; font-weight: 800; color: var(--navy); font-variant-numeric: tabular-nums; }
.nav-digital-unit { font-size: 0.65rem; color: var(--navy-soft); margin-left: 3px; font-weight: 600; }
/* Light-theme counterpart to .eng-digital-sub, for tiles (Speed, Odometer,
   Attitude) whose digital-block styling now depends on which page they're
   on - dark/instrument-style on the Engine page, matching this app's own
   dark-cluster look, light everywhere else, matching the rest of the app. */
.nav-digital-sub { font-size: 0.58rem; color: var(--label); font-weight: 700; letter-spacing: 0.03em; margin-top: 4px; }
.nav-digital-sub b { color: var(--navy-soft); font-weight: 700; }

/* ---------- Weather / forecast panels ---------- */
.weather-main { display: flex; align-items: baseline; gap: 18px; }
.weather-temp-block { display: flex; align-items: baseline; gap: 2px; }
.weather-temp { font-size: 1.8rem; font-weight: 800; color: var(--navy); }
.weather-drop { font-size: 1.1rem; margin-right: 1px; }
.weather-icon { font-size: 1.6rem; line-height: 1; }
.weather-condition { font-size: 0.85rem; font-weight: 700; color: var(--navy); }
.weather-rows { margin-top: 6px; }
.weather-row {
  display: flex;
  justify-content: space-between;
  font-size: 0.78rem;
  padding: 2px 0;
  color: var(--navy-soft);
}
.weather-row b { color: var(--navy); }
.weather-row b.trend-warning { color: var(--red); }

.pressure-section {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  min-height: 84px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.pressure-section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.pressure-section-title {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--label);
}
.pressure-current {
  font-size: 1.1rem;
  font-weight: 800;
  color: var(--navy);
  font-variant-numeric: tabular-nums;
}
.pressure-section .weather-row { padding: 2px 0 0; }
.cape-section { padding-top: 6px; justify-content: flex-start; }
/* The header row is space-between (built to also fit a value on the right,
   like the Barometric Pressure section reuses), but this one's title is its
   only child, so left it'd sit flush left - now that the tile has a fixed
   width wider than the title text, that reads as off-center. */
.cape-section .pressure-section-header { justify-content: center; }
.cape-section-title {
  font-size: 0.95rem;
  font-weight: 800;
  color: var(--navy);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.cape-status-wrap {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.cape-status {
  text-align: center;
  font-weight: 800;
  font-size: 1.3rem;
  color: var(--green);
  /* Reserves the same height as the 2-line warning phrasing ("Potential
     Thunder" / "Check Forecast") even for the 1-line "Safe" case - without
     this the whole tile grew/shrank every time thunder risk flipped
     between the two, on top of the .cape-status-time line below doing the
     same thing (see its own placeholder in renderForecastHtml). */
  min-height: 56px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.cape-status.warning { color: var(--red); }
.cape-status-time {
  text-align: center;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--red);
  margin-top: 3px;
}
.cape-status-value {
  text-align: center;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--label);
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.pressure-chart {
  display: block;
  width: 296px;
  height: 190px;
  margin-top: 8px;
}
.pressure-chart-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 296px;
  /* Matches .pressure-chart's own height exactly, not just roughly - a
     smaller placeholder still let the Live Weather tile visibly grow once
     enough pressure samples arrived (a few minutes after every reboot) to
     swap the placeholder for the real chart. */
  height: 190px;
  margin-top: 8px;
  color: var(--label);
  font-size: 0.75rem;
  font-style: italic;
  text-align: center;
}
/* The barometer warning overlays the bottom edge of the chart area instead
   of sitting below it, so the tile never changes height when an alarm
   comes and goes - see renderWeatherPanel for why that matters on a
   freeform layout. The wrap is the positioning context (scoped here, not
   on .panel, so no other tile's absolute children are affected). */
.weather-chart-wrap { position: relative; }
.weather-warning {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 6px;
  background: var(--red-bg);
  color: var(--red);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 0.75rem;
  font-weight: 700;
  text-align: center;
}

/* ---------- Forecast panel top row (icon + temp/condition + wind/precip) ---------- */
.forecast-top {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 84px;
}
.forecast-top .weather-icon { flex-shrink: 0; }
.forecast-top .weather-icon svg { display: block; }
/* min-width:0 overrides the flex default of min-width:auto, which would
   otherwise keep this item (and therefore the whole shrink-to-fit tile) at
   least as wide as the condition text's natural one-line width - a long
   forecast string ("Slight Chance Showers And Thunderstorms") would
   otherwise force the tile wider instead of wrapping. */
.forecast-main { flex: 1; min-width: 0; }
.weather-condition { white-space: normal; }
/* Locks the tile to a stable width instead of shrink-to-fit, so no content
   (long condition text, long combo-row labels, etc.) can ever push it wide
   enough to overlap whatever tile sits to its right. */
#card-forecastWeather { width: 336px; }
.forecast-main .weather-main { display: flex; align-items: baseline; gap: 2px; }
.forecast-main .weather-temp { font-size: 1.7rem; font-weight: 800; color: var(--gold); }
.weather-condition { font-size: 0.72rem; font-weight: 800; letter-spacing: 0.02em; color: var(--navy); margin-top: 1px; }
.weather-hilo { font-size: 0.72rem; color: var(--navy-soft); margin-top: 2px; }
.forecast-side { text-align: right; flex-shrink: 0; }
.forecast-wind { font-size: 0.72rem; font-weight: 700; color: var(--blue); white-space: nowrap; }
.forecast-precip { font-size: 0.68rem; color: var(--navy-soft); white-space: nowrap; margin-top: 2px; }

/* ---------- Tide Now (inside forecast panel) ---------- */
.tide-now-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  min-height: 84px;
}
.tide-now-label { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.05em; color: var(--green); }
.tide-now-value { font-size: 1.5rem; font-weight: 800; color: var(--green); }
.tide-now-value .metric-unit { font-size: 0.75rem; color: var(--green); margin-left: 2px; }
.tide-trend { font-size: 0.72rem; font-weight: 700; text-transform: capitalize; color: var(--green); }
.tide-trend.falling { color: var(--red); }
.tide-detail { text-align: right; }
.tide-detail-row { font-size: 0.72rem; color: var(--navy-soft); }
.tide-detail-row b { color: var(--navy); display: block; }
.tide-detail-row.high span { color: var(--green); }
.tide-detail-row.low span { color: var(--gold); }
/* Today's tide curve with a "now" marker (see tideChartSvg) - sized to
   match .pressure-chart's width so the two charts read as a matched pair. */
.tide-chart-wrap { margin-top: 4px; }
.tide-chart {
  display: block;
  width: 296px;
  height: 100px;
  /* Dragging on the graph scrubs the marker through the day (see
     bindTideScrub) - `none` stops a touch drag being taken as a page
     scroll, overriding .panel's `manipulation` for this one element. */
  touch-action: none;
  cursor: ew-resize;
}
/* Fills the tide-now-row/cape-section's own reserved min-height (see
   .tide-now-row and .pressure-section) while the tide/CAPE fetch is still
   in flight, instead of omitting the whole block - the tile's overall size
   used to visibly jump once each fetch resolved a few seconds after load,
   since fit-content sizing means the tile's on-screen footprint IS its
   content's natural size. */
.forecast-gathering { width: 100%; text-align: center; color: var(--label); font-size: 0.75rem; font-style: italic; }

/* ---------- Nearby vessels (AIS) panel ---------- */
/* A real AIS-broadcast vessel name can run much longer than the demo/
   fallback names ("MMSI 367123456", "Unknown") this was mostly tested
   against - without a locked card width, a long name entering range (or an
   existing target aging out and being replaced by one) would grow the
   whole fit-content tile. #card-nearbyVessels{width} below (paired with
   EXPLICIT_WIDTH_PANELS in app.js, same fix as forecastWeather/tankLevels/
   engineService/hullFouling) locks the tile itself; min-width:0 + ellipsis
   truncation here handles names too long to fit even that fixed width. */
#card-nearbyVessels { width: 340px; }
.ais-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 8px 10px;
  margin-bottom: 6px;
  background: var(--bg);
  border-radius: 8px;
}
.ais-row:last-child { margin-bottom: 0; }
.ais-row.ais-stale { opacity: 0.55; }
.ais-row.ais-close { background: var(--red-bg); }
.ais-row.ais-close .ais-name,
.ais-row.ais-close .ais-distance { color: var(--red); }
.ais-name-block { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.ais-name { font-size: 0.8rem; font-weight: 700; color: var(--navy); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ais-age { font-size: 0.66rem; color: var(--label); flex-shrink: 0; }
.ais-value-block { display: flex; flex-direction: column; align-items: flex-end; flex-shrink: 0; }
.ais-distance { font-size: 0.85rem; font-weight: 800; color: var(--green); white-space: nowrap; }
.ais-speed { font-size: 0.65rem; color: var(--label); white-space: nowrap; }

/* ---------- Dynamic (battery/solar/engine) panel ---------- */
.dynamic-list { margin-top: 2px; }
.dynamic-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 3px 0;
}
.dynamic-row.alarm { color: var(--red); font-weight: 700; }
.dynamic-id { color: var(--label); width: 28px; font-size: 0.85rem; }
.dynamic-value { font-size: 1.7rem; font-variant-numeric: tabular-nums; font-weight: 800; color: var(--navy); }
.dynamic-row.alarm .dynamic-value { color: var(--red); }
.dynamic-unit { color: var(--label); font-size: 0.85rem; }

/* ---------- Compass / gauge panel ---------- */
.panel-compass {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 12px 10px;
}

/* Wind compass only (not the engine's RPM gauge, which keeps the plain
   layout above): the NOAA chart becomes the full card background, with the
   title/AWA badge/ring/gust pills all overlaid on top of it. */
.panel-wind-compass {
  position: relative;
  padding: 0;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  width: 100%;
  max-width: 616px;
  margin: 0 auto;
}
.panel-wind-compass > .panel-title {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  z-index: 3;
  margin: 0;
}
.compass-chart-wrap {
  position: absolute;
  inset: -50%;
  overflow: hidden;
  z-index: 0;
}
.compass-chart-wrap img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.panel-wind-compass .compass-awa-badge {
  position: absolute;
  top: 16px;
  left: 14px;
  z-index: 3;
  margin-bottom: 0;
}
.panel-wind-compass .compass-dial-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
}
.panel-wind-compass .gust-row {
  position: absolute;
  bottom: 12px;
  left: 0;
  right: 0;
  z-index: 3;
  margin-top: 0;
}

/* One combined badge rather than two competing for corner space - AWA is
   always the first row; a second TRUE row is added underneath only when
   true wind data actually exists. */
.compass-awa-badge {
  display: inline-flex;
  flex-direction: column;
  gap: 4px;
  background: var(--gold-bg);
  color: var(--gold);
  padding: 8px 14px;
  border-radius: 8px;
  margin-bottom: 10px;
}
.awa-row { display: flex; align-items: baseline; gap: 8px; }
/* Distinct color from AWA (gold) - same green as the true wind needle, so
   the badge row and needle read as the same instrument at a glance. */
.twa-row { color: var(--green); }
.awa-label { font-size: 0.78rem; font-weight: 700; letter-spacing: 0.05em; }
.awa-value { font-size: 1.3rem; font-weight: 800; }
/* Shrunk a bit from the full 100% - only used by the wind compass (the RPM
   gauge builds its own dial without this wrapper) - so the AWA/TRUE badge
   and the zoom/map-source buttons all have a bit of clear margin in their
   corners instead of crowding the ring's own edge. */
.compass-dial-wrap { position: relative; width: 88%; max-width: 480px; margin: 0 auto; }
.compass-svg { display: block; width: 100%; aspect-ratio: 1 / 1; }
.compass-readout {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  pointer-events: none;
  text-shadow: 0 0 6px var(--svg-halo), 0 0 6px var(--svg-halo), 0 0 10px var(--svg-halo);
}
.compass-side {
  font-size: 1.2rem;
  font-weight: 800;
  letter-spacing: 0.05em;
}
.compass-side.port { color: var(--blue); }
.compass-side.stbd { color: var(--blue); }
.compass-value {
  font-size: 3.4rem;
  font-weight: 800;
  color: var(--gold);
  line-height: 1;
}
.compass-unit {
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--navy-soft);
  text-transform: uppercase;
}
.gust-row {
  display: flex;
  gap: 10px;
  margin-top: 12px;
  width: 100%;
  padding: 0 12px;
  justify-content: space-between;
}
.gust-pill {
  background: var(--red-bg);
  color: var(--red);
  border-radius: 8px;
  padding: 4px 10px;
  font-size: 0.68rem;
  font-weight: 700;
  text-align: center;
}
.gust-pill b { font-size: 0.82rem; }
/* Not a warning like the gust pills either side of it - same green as the
   true wind needle/badge, so all three read as the same instrument. */
.gust-pill.true-wind-pill { background: var(--teal-bg); color: var(--green); }

/* ---------- Battery & Power panel ---------- */
/* House SOC + Time to Go are the same bank's data and belong together
   visually - a tinted, bordered group instead of plain dark tiles keeps
   them reading as one block, distinct from Starting Battery/Generator
   below (which share the ordinary dark .power-tile treatment). Without
   this, every .power-tile has the identical near-black background, so
   Time to Go visually disappears into whatever tile follows it. */
.power-house-group {
  background: var(--teal-bg);
  border: 1px solid var(--green);
  border-radius: 10px;
  padding: 10px 12px 8px;
  margin-bottom: 10px;
}
.power-house-group .power-tile.power-tile-wide {
  background: transparent;
  padding: 6px 0 0;
}
.power-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.power-soc { line-height: 1; }
.power-soc-value { font-size: 2.6rem; font-weight: 800; color: var(--gold); font-variant-numeric: tabular-nums; }
.power-soc-unit { font-size: 1.3rem; font-weight: 700; color: var(--gold); }
.power-soc-voltage { font-size: 0.85rem; font-weight: 700; color: var(--navy-soft); margin-top: 4px; font-variant-numeric: tabular-nums; }
.power-charging-badge {
  background: var(--teal-bg);
  color: var(--green);
  border-radius: 8px;
  padding: 6px 10px;
  text-align: right;
  min-width: 90px;
}
.power-charging-label { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.05em; }
.power-charging-amps, .power-charging-watts { font-size: 0.95rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.power-progress-track {
  height: 8px;
  background: var(--border);
  border-radius: 4px;
  margin: 8px 0 10px;
  overflow: hidden;
}
.power-progress-fill { height: 100%; background: var(--gold); border-radius: 4px; }

/* ---------- Tank levels panel ---------- */
.tank-row { margin-bottom: 10px; }
.tank-row:last-child { margin-bottom: 0; }
/* 2-column grid instead of 5 tanks stacked one under another - makes better
   use of the tile's width instead of just growing taller and taller as
   tanks are added. The odd one out (5 tanks in 2 columns) spans the full
   width on its own row rather than leaving an empty cell beside it - which
   also happens to be Black Water, the one row with an extra "flushes left"
   line, so it's the one that benefits most from the extra room. */
#card-tankLevels { width: 420px; }
#card-tankLevels .panel-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 18px;
  row-gap: 10px;
}
#card-tankLevels .tank-row { margin-bottom: 0; }
#card-tankLevels .tank-row:last-child:nth-child(odd) {
  grid-column: 1 / -1;
  justify-self: center;
  width: 100%;
  max-width: calc(50% - 9px); /* same width as one normal column, just centered on its own row */
}
.tank-row-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 4px;
}
.tank-label { font-size: 0.8rem; font-weight: 700; color: var(--navy); }
.tank-pct { font-size: 0.8rem; font-weight: 800; color: var(--navy-soft); font-variant-numeric: tabular-nums; }
.tank-bar-track {
  height: 10px;
  background: var(--border);
  border-radius: 5px;
  overflow: hidden;
}
.tank-bar-fill { height: 100%; background: var(--blue); border-radius: 5px; }
.tank-bar-fill.alarm-fill { background: var(--red); }
.tank-volume { font-size: 0.7rem; color: var(--label); font-weight: 600; margin-top: 3px; }
.tank-flushes { font-size: 0.7rem; color: var(--label); font-weight: 600; text-align: center; }
.power-grid {
  display: grid;
  /* auto-fit rather than a hard 1fr 1fr - Solar/AC can each independently be
     hidden (not discovered and demo off, or manually hidden in Setup), so
     this needs to look right whether it ends up with two tiles, one, or
     (via the parent's own conditional) never rendered at all. */
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 8px;
  margin-bottom: 8px;
}
.power-tile {
  background: var(--bg);
  border-radius: 8px;
  padding: 6px 10px;
  min-width: 120px;
}
.power-tile-wide { min-width: 0; }
.power-tile-label { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--label); }
.power-tile-value { font-size: 1.15rem; font-weight: 800; color: var(--navy); font-variant-numeric: tabular-nums; }
.power-tile-value-lg { font-size: 1.3rem; color: var(--green); }
.power-tile-sub {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--gold);
  background: var(--gold-bg);
  border-radius: 5px;
  padding: 1px 6px;
  margin-left: 6px;
  /* Reserves roughly the width of a real rate ("+3.3%/hr") - this pill used
     to be omitted outright for the ~72s it takes real SOC history to
     accumulate after every load/reconnect (demo mode's fixed rate bypasses
     that delay, which is why this was easy to miss), so the tile visibly
     grew once it appeared. Now always rendered, blank until then. */
  min-width: 58px;
  text-align: center;
}
.power-tile-value-lg .power-tile-sub { color: var(--green); background: var(--teal-bg); }
.power-tile-sub-empty { background: transparent; }
.power-gen-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border-radius: 8px;
  padding: 6px 10px;
  margin-top: 8px;
}
.power-gen-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--label);
  flex-shrink: 0;
}
.power-gen-dot.running { background: var(--green); }
.power-gen-label { font-size: 0.78rem; font-weight: 700; color: var(--navy-soft); flex: 1; }
.power-gen-status { font-size: 0.78rem; font-weight: 800; color: var(--label); letter-spacing: 0.04em; }
.power-gen-dot.running ~ .power-gen-status { color: var(--green); }

/* Emporia Energy Monitor - Total up top (same "big number" treatment as
   other headline readouts), then a plain list of named circuits below,
   since the circuit count varies per install (this app never hardcodes
   how many exist) rather than a fixed grid like Power System's charger
   tiles use. */
.emporia-main-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding-bottom: 8px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.emporia-main-label { font-size: 0.8rem; font-weight: 700; color: var(--label); text-transform: uppercase; letter-spacing: 0.04em; }
.emporia-main-value { font-size: 1.6rem; font-weight: 800; color: var(--navy); }
.emporia-main-value small { font-size: 0.9rem; font-weight: 700; color: var(--navy-soft); margin-left: 2px; }
.emporia-circuit-list { display: flex; flex-direction: column; gap: 2px; }
.emporia-circuit-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 5px 2px;
  font-size: 0.85rem;
}
.emporia-circuit-name { color: var(--navy-soft); font-weight: 600; }
.emporia-circuit-watts { font-weight: 800; color: var(--label); }
.emporia-circuit-watts.active { color: var(--navy); }
.emporia-balance-row {
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px dashed var(--border);
}
.emporia-balance-row .emporia-circuit-name, .emporia-balance-row .emporia-circuit-watts { color: var(--label); font-style: italic; }

/* A launcher tile (Victron Remote Console) - a big tappable card rather
   than a data readout, so it gets its own button-shaped treatment instead
   of borrowing a metric tile's layout. */
.victron-console-launch {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 24px 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-family: inherit;
  text-align: center;
}
/* Only the two real buttons (the main launch, and "tap to try again") get
   pointer/press feedback - the plain <div> "looking..." state uses this
   same class for identical layout but isn't actually clickable. */
button.victron-console-launch { cursor: pointer; }
button.victron-console-launch:active { background: var(--border); }
.victron-console-icon { font-size: 2rem; line-height: 1; }
.victron-console-label { font-size: 0.95rem; font-weight: 700; color: var(--navy); }
.victron-console-sub { font-size: 0.75rem; color: var(--label); }
.victron-console-status .victron-console-label { color: var(--label); font-weight: 600; }

/* ---------- Anchor watch panel ---------- */
/* Fixed width rather than letting it shrink-to-fit its content: the radar
   map is aspect-ratio 1:1, so without this, any content-driven width change
   between anchored/idle (longer labels, an AUTO badge, extra digits) would
   translate into a HEIGHT change too via the aspect-ratio, which is exactly
   what pushed whatever tile sits below this one out of place. */
/* Default/natural width - applyPanelSizing() in app.js overrides this
   directly via inline style once the tile has ever been resized, using this
   same 405px as its 100% reference point. */
#card-anchorWatch { width: 405px; }
.anchor-radar-wrap {
  position: relative;
  background: var(--bg);
  border-radius: 10px;
  overflow: hidden;
  margin: 8px 0;
  aspect-ratio: 1 / 1;
}
.anchor-radar-chart {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.anchor-radar-svg { position: relative; width: 100%; height: 100%; display: block; }
.boat-icon-marker { cursor: pointer; }
.boat-icon-picker {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.boat-icon-choice {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  color: var(--navy);
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
}
.boat-icon-choice:active { background: var(--border); }
.boat-icon-choice svg { width: 22px; height: 22px; flex-shrink: 0; }
.btn-reset-trail {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--navy-soft);
  font-size: 0.68rem;
  font-weight: 700;
  padding: 4px 8px;
  border-radius: 6px;
  cursor: pointer;
  opacity: 0.9;
}
.btn-reset-trail:active { background: var(--border); }

.radar-zoom-controls {
  position: absolute;
  bottom: 8px;
  right: 8px;
  z-index: 2;
  display: flex;
  gap: 4px;
}
.radar-zoom-controls button {
  width: 30px;
  height: 30px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--navy);
  font-size: 1rem;
  font-weight: 700;
  border-radius: 6px;
  cursor: pointer;
  opacity: 0.9;
  line-height: 1;
}
.radar-zoom-controls button:active { background: var(--border); }
.compass-zoom-controls {
  top: 12px;
  bottom: auto;
  right: 12px;
  z-index: 3;
}
.map-source-toggle {
  position: absolute;
  z-index: 3;
  width: 30px;
  height: 30px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  opacity: 0.9;
  display: flex;
  align-items: center;
  justify-content: center;
}
.map-source-toggle:active { background: var(--border); }
.anchor-map-toggle { top: 8px; left: 8px; }
.compass-map-toggle { top: 50px; right: 12px; }
.anchor-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  text-align: center;
  margin-bottom: 6px;
}
.anchor-stat-label {
  font-size: 0.68rem;
  color: var(--label);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.anchor-stat-value {
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--navy);
}
.anchor-radius-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.85rem;
  color: var(--navy-soft);
}
.anchor-radius-row input {
  width: 64px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--navy);
  border-radius: 6px;
  padding: 4px 6px;
}
.rode-stepper {
  display: flex;
  align-items: center;
  gap: 6px;
}
.rode-stepper button {
  width: 28px;
  height: 28px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--navy);
  font-size: 1rem;
  font-weight: 700;
  border-radius: 6px;
  cursor: pointer;
  line-height: 1;
}
.rode-stepper button:active { background: var(--border); }
.rode-value {
  min-width: 52px;
  text-align: center;
  font-weight: 700;
  color: var(--navy);
}
.anchor-combo-row {
  gap: 8px;
  flex-wrap: nowrap;
  white-space: nowrap;
}
.anchor-combo-group {
  display: flex;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px 6px;
  background: var(--bg);
}
/* A placeholder Radius group shown pre-drop, disabled - purely so the row
   takes up the same width whether anchored or not. The radar map below is
   aspect-ratio 1:1, so any width difference between states would otherwise
   show up as a height difference too, which is exactly what pushed
   whatever's below this tile out of place when dropping anchor. */
.anchor-combo-disabled { opacity: 0.4; }
.anchor-combo-disabled button { cursor: default; }
.anchor-combo-row .rode-stepper {
  gap: 2px;
}
.anchor-combo-row .rode-stepper button {
  width: 21px;
  height: 21px;
  font-size: 0.85rem;
}
.anchor-combo-row .rode-value {
  min-width: 24px;
  font-size: 0.85rem;
}
.rode-unit {
  font-size: 0.6em;
  font-weight: 600;
  color: var(--label);
  margin-left: 1px;
}
.auto-badge {
  font-size: 0.62rem;
  font-weight: 700;
  color: var(--teal);
  background: var(--teal-bg, var(--bg));
  border-radius: 4px;
  padding: 1px 5px;
  margin-left: 4px;
  vertical-align: middle;
}

/* ---------- Buttons ---------- */
.btn {
  margin-top: 10px;
  background: var(--gold);
  border: none;
  color: #fff;
  padding: 9px 16px;
  border-radius: 8px;
  font-size: 0.9rem;
  font-weight: 700;
  cursor: pointer;
  width: 100%;
}
.btn:active { filter: brightness(0.92); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-danger { background: var(--red); }

/* Press-and-hold confirmation fill - a lighter overlay grows left-to-right
   across the button as the hold progresses, so there's live feedback that
   the press registered and how much longer it needs. */
.hold-btn { position: relative; overflow: hidden; }
.hold-btn-fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0%;
  background: rgba(255, 255, 255, 0.35);
  pointer-events: none;
}
.hold-btn-label { position: relative; }

/* ---------- Windy / forecast tab ---------- */
.panel-windy { padding: 10px; }
.windy-frame {
  display: block;
  width: 100%;
  height: calc(100vh - 210px);
  min-height: 360px;
  border: 0;
  border-radius: 8px;
}

/* ---------- Chart plotter tab (Freeboard-SK) ---------- */
.panel-chart { padding: 10px; }
.chart-frame {
  display: block;
  width: 100%;
  height: calc(100vh - 210px);
  min-height: 360px;
  border: 0;
  border-radius: 8px;
}

/* ---------- Radar repeater tab (synthetic demo sweep - no hardware yet) ---------- */
.panel-radar { padding: 10px; }
.radar-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: #05080a;
  border-radius: 8px;
  padding: 16px;
  min-height: calc(100vh - 210px);
}
/* Fixed amber warning color, not a themed token - a safety caption like
   this should read the same regardless of day/night styling, same
   reasoning as the port/stbd rudder colors staying fixed literals. */
.radar-badge {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  color: #ffb020;
  background: rgba(255, 176, 32, 0.12);
  border: 1px solid rgba(255, 176, 32, 0.4);
  border-radius: 6px;
  padding: 4px 12px;
}
.radar-canvas {
  display: block;
  width: min(100%, calc(100vh - 320px));
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  border: 1px solid var(--media-lcd-glow-soft);
  background: #000;
}
.radar-controls { display: flex; align-items: center; gap: 14px; }
.radar-btn {
  background: var(--media-lcd-glow-soft);
  border: 1px solid var(--media-lcd-dim);
  color: var(--media-lcd);
  border-radius: 6px;
  padding: 8px 14px;
  font-size: 0.8rem;
  font-weight: 700;
  cursor: pointer;
}
.radar-range-readout {
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  color: var(--media-lcd);
  font-size: 0.95rem;
}

/* ---------- Camera feed tile ---------- */
/* An ordinary draggable/resizable tile (see discoveredCameraTiles/
   renderCameraFeedPanel in app.js) - sized like every other gauge-style
   tile (fit-content + the shared continuous zoom scale, see
   applyPanelSizing), just with a fixed natural width/aspect-ratio box
   instead of an SVG dial. 16:9 is just this box's own default shape (drag
   the resize grip to change it, like any tile) - the video itself always
   fills whatever shape the tile actually is (object-fit: cover, below),
   never letterboxed, regardless of the camera's own native resolution. */
.panel-camera-feed { padding: 8px; }
.panel-camera-feed .panel-body {
  width: 360px;
  aspect-ratio: 16 / 9;
  background: #05080a;
  border-radius: 8px;
  overflow: hidden;
}
/* video-stream (go2rtc's own web component, loaded directly into this page
   rather than via iframe - see renderCameraFeedPanel) puts its <video> in
   light DOM with no class of its own, so a plain descendant selector reaches
   it directly. It does set its own inline width/height: 100% already; cover
   is the one thing it doesn't set, which is exactly what's needed here. */
.cam-tile-frame {
  display: block;
  width: 100%;
  height: 100%;
  background: #000;
}
.cam-tile-frame video {
  object-fit: cover;
}
/* Double-click-the-title-to-maximize (see toggleTileMaximize) - a custom
   "fill the screen" toggle instead of the native Fullscreen API, which the
   kiosk's own Chromium rejects. Shared by every tile. !important overrides
   the inline position/left/top layoutPanelsFreeform() sets for a tile's
   normal place in the page.

   Two flavours, decided in toggleTileMaximize: a tile whose content is
   inherently fill-shaped (a camera's video, the Victron console's iframe)
   just has its body stretched to 100% (the per-tile rules further down);
   every other tile - a readout, a gauge, a grid - is instead scaled up as a
   whole to fit the screen (tile-maximized-fit), since stretching a
   fit-content box to 100vw would just leave the same small content sitting
   in the corner of a huge empty card. */
.panel.tile-maximized {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  /* Some tiles cap their own card size - the Anchor Overview Map is
     max-width:616px + aspect-ratio:1/1 (.panel-wind-compass). max-width
     still clamps a width:100vw card, and aspect-ratio would fight the
     explicit height, so on a screen wider than the cap (the kiosk) the
     "fullscreen" tile sat at 616px pinned to the left. Release both here so
     the card genuinely fills the viewport; the tile's own content is
     centered and scaled to fit by tile-maximized-fit + toggleTileMaximize. */
  max-width: none !important;
  min-width: 0 !important;
  aspect-ratio: auto !important;
  margin: 0;
  z-index: 200;
  padding: 0;
  border-radius: 0;
  /* A tile in a no-data state is dimmed to 0.55 (.panel.no-data) - which,
     left in place while maximized, made the whole full-screen overlay
     55% transparent, so the header and other tiles showed straight
     through it (confirmed on a real iPhone maximizing the anchor tile
     while it read "NO GPS DATA"). A maximized tile must be fully opaque
     to actually cover the screen. !important because .panel.no-data is
     equal specificity. */
  opacity: 1 !important;
}
.panel.tile-maximized-fit {
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Shrink-wrap the zoomed wrapper to its content so toggleTileMaximize can
   measure the tile's natural size (and so the flex centering above has a
   real box to center), instead of it stretching to the full card width
   like a normal block child would. */
.panel.tile-maximized-fit .panel-zoom {
  width: fit-content;
  flex: none;
}
/* Full-screen black layer behind a maximized tile that doesn't itself cover
   the whole screen (the square Anchor Overview Map leaves bars at the
   sides). Body-level (see showMaximizeBackdrop), just under the card. */
#tile-maximize-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 199;
  background: #05080a;
}
/* An aspect (square) instrument maximizes as the biggest SQUARE that fits the
   screen, centered - keeping its ring/map/badges laid out in the square they
   were designed for, instead of being stretched into a full-screen rectangle
   that clipped the ring top and bottom. min(100vw,100vh) fits whichever
   screen dimension is smaller; 0.92 leaves a small margin. The black
   backdrop above fills the rest. Both width and height are set, so the
   tile's own aspect-ratio rule is moot. */
.panel.tile-maximized.tile-maximized-aspect {
  width: calc(min(100vw, 100vh) * 0.92) !important;
  height: calc(min(100vw, 100vh) * 0.92) !important;
  max-width: none !important;
  left: 50% !important;
  top: 50% !important;
  transform: translate(-50%, -50%);
}
/* The always-reachable exit from a maximized tile (see showMaximizeExit).
   z-index sits above the maximized card (200); appended to <body>, outside
   #viewport-root, so it stays put at the true screen corner no matter how
   the tile itself is scaled/overflowing. Generous hit area + safe-area
   insets so it clears an iPhone's notch/rounded corner. */
#tile-maximize-exit {
  display: none;
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  right: calc(env(safe-area-inset-right, 0px) + 10px);
  z-index: 300;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  line-height: 1;
  color: #fff;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 50%;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#tile-maximize-exit:active { background: rgba(0, 0, 0, 0.8); }
.panel-camera-feed.tile-maximized .panel-body {
  width: 100% !important;
  height: 100% !important;
  aspect-ratio: unset;
  border-radius: 0;
}
.panel-camera-feed.tile-maximized .panel-title {
  position: absolute;
  top: 10px;
  left: 14px;
  z-index: 1;
  background: rgba(0, 0, 0, 0.5);
  padding: 4px 10px;
  border-radius: 6px;
}

/* The embedded Victron GX console (a real Qt/WebAssembly app, running
   through this dashboard's own proxy - see renderVictronConsolePanel).
   Sized to exactly 1024x600 - a real Cerbo/GX Touch 50 screen's actual
   native resolution (confirmed via the app's own `aspect-ratio: 1024/600`
   CSS rule) - rather than some arbitrary tile size, so the app renders at
   its true native size with no letterboxing or awkward reflow. Too big to
   sit at that literal size next to normal tiles, so it starts already
   shrunk down via this dashboard's existing continuous tile-resize zoom
   (drag the corner grip, same as any other tile) rather than fighting the
   app's own responsive layout by giving it a smaller, mismatched
   container instead. Double-click the title to fill the screen (see
   toggleTileMaximize) for anything beyond a glance - the same gesture
   camera tiles already use, and for the same reason: a tile-sized view is
   fine to look at, too small to actually use. */
.panel-victron-console { padding: 8px; }
/* 1024x600 matches the embedded app's own native resolution (see the
   iframe comment above) - fine on the kiosk's own wide screen, but a
   literal 1024px is wider than a phone's entire viewport. This tile's
   card sizing goes through applyPanelSizing()'s generic 'fit-content'
   path (it isn't in EXPLICIT_WIDTH_PANELS), which shrink-wraps the card
   to whatever width panel-body actually computes to - so capping that
   here is enough on its own, no JS or per-device special-casing needed.
   Deliberately NOT real viewport units (92vw) - everything under
   #viewport-root already gets uniformly zoomed by appScale
   (applyViewportScale in app.js), so a plain vw value would be scaled
   down a second time by that zoom, landing far smaller than intended.
   --design-viewport-width is that same root's own PRE-zoom width, set
   by applyViewportScale - a fraction of it scales down together with
   everything else instead of fighting the zoom. aspect-ratio keeps this
   proportional as it shrinks rather than distorting/cropping. */
.panel-victron-console .panel-body {
  width: min(1024px, calc(0.92 * var(--design-viewport-width, 1024px)));
  aspect-ratio: 1024 / 600;
  border-radius: 8px;
  overflow: hidden;
}
.victron-console-frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
/* The card-level fullscreen rule itself is the shared .panel.tile-maximized
   (see the camera tile's section) - only the fill-mode specifics live here.
   .panel-zoom (the title+body wrapper - see its own comment elsewhere in
   this file) has no height of its own by default, sized purely by its
   content; .panel-body's height:100% below has nothing real to resolve
   against without this, and silently collapses instead of actually
   filling the maximized card. */
.panel-victron-console.tile-maximized .panel-zoom {
  height: 100%;
}
.panel-victron-console.tile-maximized .panel-body {
  width: 100% !important;
  height: 100% !important;
  border-radius: 0;
}
.panel-victron-console.tile-maximized .panel-title {
  position: absolute;
  top: 10px;
  left: 14px;
  z-index: 1;
  background: rgba(0, 0, 0, 0.5);
  padding: 4px 10px;
  border-radius: 6px;
}

/* ---------- Settings > Cameras rows ---------- */
.cam-item-row { display: flex; align-items: center; gap: 8px; }
.cam-item-status {
  font-size: 0.7rem;
  color: var(--label);
  flex-shrink: 0;
}
.cam-item-status.cam-status-ok { color: var(--green); }
.cam-item-row .cam-item-label { flex: 1; min-width: 0; }
.cam-item-row .cam-item-url { flex: 2; min-width: 0; font-family: monospace; font-size: 0.78rem; }
.cam-item-open-admin {
  background: var(--teal-bg);
  color: var(--blue);
  border: none;
  border-radius: 6px;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  font-size: 0.85rem;
  cursor: pointer;
}
.cam-item-row-sub { padding-top: 0 !important; margin-top: -6px; }
.cam-item-relay-label { font-size: 0.72rem; color: var(--label); font-weight: 600; display: flex; align-items: center; gap: 6px; cursor: help; }
.cam-item-hd-url { width: 100%; box-sizing: border-box; font-family: monospace; font-size: 0.72rem; opacity: 0.85; }

/* ---------- Settings > ONVIF scan results ---------- */
.onvif-result-row { display: flex; align-items: center; gap: 8px; }
.onvif-result-info { flex: 1; min-width: 0; font-size: 0.85rem; font-weight: 700; color: var(--navy); }
.onvif-result-ip { font-size: 0.72rem; font-weight: 600; color: var(--label); font-family: monospace; margin-left: 6px; }
/* The generic .btn class is width:100% (meant for a full-width row of its
   own) - undoing that here, since as a flex sibling of .onvif-result-info
   it would otherwise claim most of the row and squeeze the camera's name
   down to a sliver regardless of how short "+ Add" itself is. */
.onvif-result-add { flex: none; width: auto; margin-top: 0; padding: 6px 12px; }

/* ---------- Ship's logbook tab (also hosts the Service log, via .log-subview-toggle) ---------- */
.panel-logbook { padding: 10px; }
.log-subview-toggle { display: flex; gap: 6px; margin-bottom: 14px; }
.log-subview-btn {
  flex: 1;
  padding: 9px 14px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--label);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
}
.log-subview-btn.active { background: var(--navy); color: var(--card-bg); border-color: var(--navy); }
.log-toolbar { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; flex-wrap: wrap; }
.btn-log-now {
  background: var(--navy);
  color: var(--card-bg);
  border: none;
  border-radius: 8px;
  padding: 10px 16px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
}
.btn-log-now:active { opacity: 0.8; }
.btn-log-export {
  background: var(--card-bg);
  color: var(--navy);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 16px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  margin-left: auto;
}
.btn-log-export:active { background: var(--teal-bg); }
.log-hint { font-size: 0.72rem; color: var(--label); font-weight: 600; }
.log-entries-wrap {
  overflow-y: auto;
  max-height: calc(100vh - 260px);
  min-height: 240px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.log-entry {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 14px;
}
.log-entry-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.log-entry-time { font-size: 0.85rem; font-weight: 800; color: var(--navy); }
.log-entry-watch { font-size: 0.78rem; font-weight: 700; color: var(--blue); }
.log-entry-conditions { font-size: 0.74rem; color: var(--label); font-weight: 600; margin-top: 4px; }
.log-entry-notes { font-size: 0.85rem; color: var(--navy); margin-top: 6px; white-space: pre-wrap; }
.log-entry-instruments { font-size: 0.7rem; color: var(--label); margin-top: 8px; line-height: 1.6; }
.log-empty { text-align: center; color: var(--label); padding: 24px; }

/* ---------- Diagnostics tab ---------- */
.panel-diagnostics { padding: 10px; }
.diag-section { margin-bottom: 22px; }
.diag-section:last-child { margin-bottom: 0; }
.diag-section-title {
  font-size: 0.82rem;
  font-weight: 800;
  color: var(--navy);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.diag-health-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}
.diag-health-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
}
.diag-health-card.diag-warn { background: var(--red-bg); border-color: var(--red); }
.diag-health-note { font-size: 0.68rem; color: var(--label); font-style: italic; margin-top: 8px; }
.diag-health-label { font-size: 0.65rem; font-weight: 700; color: var(--label); text-transform: uppercase; letter-spacing: 0.04em; }
.diag-health-value { font-size: 1.05rem; font-weight: 800; color: var(--navy); margin-top: 2px; font-variant-numeric: tabular-nums; }
.diag-warn .diag-health-value { color: var(--red); }
.diag-table-wrap { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; overflow-x: auto; }
.diag-data-wrap { max-height: 320px; overflow-y: auto; }
.diag-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.diag-table th {
  position: sticky;
  top: 0;
  background: var(--card-bg);
  color: var(--label);
  text-transform: uppercase;
  font-size: 0.63rem;
  letter-spacing: 0.05em;
  font-weight: 700;
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.diag-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); color: var(--navy); white-space: nowrap; }
.diag-table tr:last-child td { border-bottom: none; }
.diag-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
.diag-dot-ok { background: var(--green); }
.diag-dot-stale { background: var(--label); }

/* ---------- Logbook entry modal ---------- */
.log-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
}
.log-modal {
  background: var(--card-bg);
  border-radius: 14px;
  padding: 20px 24px;
  width: 560px;
  max-width: 92vw;
  max-height: 88vh;
  overflow-y: auto;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}
.log-modal-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; }
.log-modal-title { font-size: 1.05rem; font-weight: 800; color: var(--navy); }
.log-modal-time { font-size: 0.8rem; color: var(--label); font-weight: 600; }
.log-modal-snapshot {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 16px;
  background: var(--teal-bg);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 0.78rem;
  color: var(--navy);
  margin-bottom: 16px;
}
.log-modal-snapshot b { color: var(--label); font-weight: 700; margin-right: 4px; }
.log-modal-snapshot-wide { grid-column: 1 / -1; }
.log-modal-form { display: flex; flex-direction: column; gap: 12px; }
.log-modal-form label { display: flex; flex-direction: column; gap: 4px; font-size: 0.75rem; font-weight: 700; color: var(--label); }
.log-modal-form input, .log-modal-form select, .log-modal-form textarea {
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--navy);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  resize: vertical;
}
.log-modal-form-row { display: flex; gap: 12px; }
.log-export-presets { display: flex; gap: 8px; margin-bottom: 16px; }
.log-export-presets button {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 12px;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--navy);
  cursor: pointer;
}
.log-export-presets button:active { background: var(--teal-bg); }
.log-export-modal .log-modal-form-row label { display: flex; flex-direction: column; gap: 4px; font-size: 0.75rem; font-weight: 700; color: var(--label); flex: 1; }
.log-export-modal .log-modal-form-row input { font-family: inherit; font-size: 0.9rem; font-weight: 600; color: var(--navy); background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; }
.log-modal-form-row label { flex: 1; }
.log-modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.btn-log-cancel, .btn-log-save {
  border: none;
  border-radius: 8px;
  padding: 10px 18px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
}
.btn-log-cancel { background: var(--bg); color: var(--navy); border: 1px solid var(--border); }
.btn-log-save { background: var(--navy); color: var(--card-bg); }
.btn-log-save:active, .btn-log-cancel:active { opacity: 0.8; }

/* ---------- Hourly rounds checklist modal ---------- */
.checklist-modal { width: 420px; }
.checklist-count { font-size: 0.8rem; color: var(--label); font-weight: 700; }
.checklist-rows { display: flex; flex-direction: column; gap: 8px; }
.checklist-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  cursor: pointer;
  user-select: none;
}
.checklist-dot {
  width: 22px;
  height: 22px;
  min-width: 22px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--card-bg);
  position: relative;
}
.checklist-row.checked .checklist-dot {
  background: var(--green);
  border-color: var(--green);
}
.checklist-row.checked .checklist-dot::after {
  content: '✓';
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--card-bg);
  font-size: 0.8rem;
  font-weight: 800;
}
.checklist-label { font-size: 0.9rem; font-weight: 700; color: var(--navy); }
.checklist-row.checked .checklist-label { color: var(--label); text-decoration: line-through; }
.checklist-row-action { background: var(--teal-bg); }
.checklist-row-arrow { margin-left: auto; color: var(--blue); font-weight: 800; font-size: 1rem; }

/* ---------- Rounds checklist tile ---------- */
.checklist-tile-hint { font-size: 0.72rem; color: var(--label); font-weight: 600; margin-bottom: 10px; }
.checklist-tile-actions { display: flex; flex-direction: column; gap: 8px; }
.checklist-tile-actions button { width: 100%; }

/* ---------- Rounds checklist editor modal ---------- */
.checklist-edit-modal { width: 460px; }
.checklist-edit-interval {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--label);
  margin-bottom: 16px;
}
.checklist-edit-interval input {
  width: 70px;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 700;
  color: var(--navy);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
}
.checklist-edit-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.checklist-edit-row { display: flex; align-items: center; gap: 8px; }
.checklist-edit-input {
  flex: 1;
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--navy);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
}
/* The label is the row's whole point (it's what tells one reminder apart
   from another) but was the most space-starved element in the row once
   sharing a line with an interval box, its unit, an instance picker, and
   a delete button - forcing it onto its own full-width line, same as the
   Alarms grid rows, gives it the room a real multi-word item name needs
   and leaves the value controls a full line of their own underneath. */
.svc-item-row { flex-wrap: wrap; row-gap: 6px; }
.svc-item-row .svc-item-label { flex: 1 1 100%; width: auto; }
/* ".svc-item-row .svc-item-interval" alone loses to the generic
   ".settings-row input[type=\"number\"]" rule below (equal class-count,
   but that rule also carries an element+attribute selector) regardless of
   source order - chaining .settings-row here (the row carries both
   classes) makes this one unambiguously more specific so 60px actually
   applies instead of silently falling back to the generic 100px. */
.settings-row.svc-item-row .svc-item-interval { width: 60px; }
.svc-item-row .svc-item-instance { flex: 1; min-width: 0; }
.checklist-edit-remove {
  background: var(--red-bg);
  color: var(--red);
  border: none;
  border-radius: 6px;
  width: 32px;
  height: 32px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
}
.btn-log-add-row {
  width: 100%;
  background: var(--bg);
  border: 1px dashed var(--border);
  border-radius: 6px;
  padding: 8px;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--navy-soft);
  cursor: pointer;
  margin-bottom: 8px;
}
.btn-log-add-row:active { background: var(--teal-bg); }

/* ---------- Tab strip ---------- */
#tab-strip-wrap {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: stretch;
  background: var(--card-bg);
  border-top: 1px solid var(--border);
  z-index: 15;
  /* Room for the iPhone home indicator when installed (see #topbar). */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
#tab-strip {
  display: flex;
  justify-content: center;
  gap: 4px;
  padding: 6px 10px;
  flex: 1;
  min-width: 0;
  /* Being inside a scrollable flex child (not position:fixed directly on
     this element anymore) is what makes overflow reachable at all - once
     there are enough pages (built-in + user-created) that the row doesn't
     fit a narrow screen, the overflowing tabs on either end used to be
     simply unreachable, nothing else on the page could ever scroll them
     into view. overflow-x alone isn't enough on a real touchscreen,
     though: dragging a tab reorders it (see touch-action:none on .tab-btn
     below), which blocks a touch-scroll gesture that starts ON a button -
     and with gap:4px between tightly-packed buttons, there's barely any
     non-button surface left to reliably start a drag-to-scroll from at
     all. Confirmed directly on a real phone: setting scrollLeft
     programmatically worked, an actual finger-drag didn't. The
     #tab-strip-scroll-left/right buttons flanking this element are the
     real fix - tapping them doesn't compete with the reorder gesture.
     overflow-x/-webkit-overflow-scrolling stay as a reasonable fallback
     for anywhere a touch-drag-to-scroll does work cleanly (a mouse-wheel
     shift-scroll on desktop, a device where dragging past a button's own
     touch-action:none still reaches the gap between two buttons). */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
}
.tab-strip-scroll-btn {
  display: none;
  flex-shrink: 0;
  width: 32px;
  background: transparent;
  border: none;
  color: var(--label);
  font-size: 1.3rem;
  font-weight: 700;
  cursor: pointer;
}
.tab-strip-scroll-btn:active { background: var(--border); }
/* Shown/hidden per-side by JS (see updateTabStripScrollButtons) based on
   whether the strip actually has more to scroll toward on that side right
   now - never both hidden with genuine overflow still present, never
   shown with nothing left to reach. */
.tab-strip-scroll-btn.visible { display: block; }
.tab-btn {
  background: transparent;
  border: none;
  color: var(--label);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 8px 18px;
  border-radius: 8px;
  cursor: pointer;
  /* A page button is press-and-hold-then-draggable (see wireTabDrag) -
     without this, a touchscreen interprets that same gesture as an attempt
     to scroll/pan the page instead of dragging the button. */
  touch-action: none;
}
.tab-btn.active {
  color: var(--gold);
  background: var(--gold-bg);
}
/* Applied only once an actual drag starts (real movement past the
   threshold), never on a plain tap - see wireTabDrag. Lifted and tilted
   just enough to read as "picked up," following the pointer via its own
   inline transform set in the pointermove handler. */
.tab-btn.tab-dragging {
  position: relative;
  z-index: 20;
  background: var(--gold-bg);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
}

/* ---------- Settings panel ---------- */
#settings-panel {
  position: fixed;
  top: 0;
  right: -480px;
  /* Capped to the viewport width so the panel never runs off the left edge
     on a phone-width screen - it stays outside the scaled #viewport-root on
     purpose (see index.html) so it's never shrunk to illegibility, but that
     means IT has to be the one to shed width when the screen is narrower
     than 460px, rather than relying on the scaling that handles every
     other narrow-screen case in this app. */
  width: min(460px, 100vw);
  height: 100%;
  background: var(--card-bg);
  border-left: 1px solid var(--border);
  padding: 76px 26px 26px;
  overflow-y: auto;
  transition: right 0.25s ease;
  z-index: 20;
  color: var(--navy);
}
#settings-panel.open { right: 0; }

#settings-close-btn {
  position: absolute;
  top: 16px;
  right: 18px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--navy);
  padding: 8px 14px;
  border-radius: 8px;
  font-size: 0.9rem;
  cursor: pointer;
}
#settings-close-btn:active { background: var(--border); }

/* Each settings group gets its own bordered card, distinct from the panel's
   own background, so the (now much longer) list of sections reads as
   separate categories at a glance instead of one long undifferentiated
   scroll of rows. */
.settings-section {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  margin-bottom: 20px;
}
.settings-section:last-child { margin-bottom: 0; }
.settings-section-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.95rem;
  font-weight: 800;
  color: var(--navy);
  letter-spacing: 0.02em;
  margin-bottom: 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

/* Top-bar category switcher: only the .settings-section matching the
   active category is shown at a time, so the panel reads as one short
   screen per category instead of one long scroll through all of them. */
.settings-cat-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: -4px 0 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.settings-cat-btn {
  flex: 1 1 calc(33.333% - 4px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--navy-soft);
  border-radius: 8px;
  padding: 8px 4px 7px;
  cursor: pointer;
}
.settings-cat-btn:active { filter: brightness(0.95); }
.settings-cat-btn.active {
  background: var(--gold);
  border-color: var(--gold);
  color: #fff;
}
.settings-cat-icon { font-size: 1.1rem; line-height: 1; }
.settings-cat-label {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-align: center;
  line-height: 1.15;
}

/* Alarms is the longest single category (13 rows) - a dense two-column
   grid keeps it from being the one screen that still scrolls forever. */
.settings-alarms-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 14px;
}
/* The column is too narrow for label + input + unit to share a line, so
   force a clean 2-line shape instead of letting flex-wrap break the three
   pieces apart wherever they happen to run out of room - unwrapped, the
   unit label could land on its own orphaned third line with no visible
   connection to the input box above it. */
.settings-alarms-grid .settings-row {
  padding: 6px 0;
  flex-wrap: wrap;
  row-gap: 3px;
}
.settings-alarms-grid .settings-row label {
  flex: 1 1 100%;
  font-size: 0.85rem;
}
.settings-alarms-grid .settings-row input[type="number"] { width: 72px; }

/* A named source-picker grouped inside a larger settings-section (e.g. two
   of these under "Data Sources") - a lighter, un-bordered heading so it
   reads as a subcategory rather than competing with the section title. */
.settings-subsection { margin-top: 16px; }
.settings-subsection:first-of-type { margin-top: 4px; }
.settings-subsection-title {
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--navy-soft);
  letter-spacing: 0.02em;
  margin-bottom: 8px;
}

.settings-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  font-size: 0.92rem;
}
.settings-row-buttons { display: block; }
.btn-system { margin-top: 0; }
.settings-row label {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
}
.settings-row input[type="number"],
.settings-row input[type="text"] {
  width: 100px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--navy);
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 0.9rem;
}
.settings-row select {
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--navy);
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 0.9rem;
}
/* Unconstrained, a <select> sizes itself to its own currently-selected
   option text - so with no width here, House/Starter/Generator each end
   up a different width (a bare "Auto (first found)" placeholder vs. a
   picked "Instance 0 (12.68V)") and the label/select boundary zig-zags
   down the three rows instead of lining up. */
#power-sources-container select { width: 190px; flex: none; }
.unit-label { color: var(--label); width: 34px; }
/* Every "number input + its unit" pair in Settings (alarm thresholds,
   depth offset, windlass height, tank capacities, gallons per flush)
   reads as one continuous field with the unit baked into the box, rather
   than a separate label floating beside it - butted together with the
   shared row gap cancelled out, leaving only the one deliberate divider
   line between the number and its unit. */
.settings-row input[type="number"]:has(+ .unit-label) {
  border-radius: 6px 0 0 6px;
  border-right: none;
}
.settings-row input[type="number"] + .unit-label {
  width: auto;
  margin-left: -10px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 0 6px 6px 0;
  padding: 5px 8px 5px 6px;
  font-size: 0.76rem;
}
/* Duplicate Senders rows: label above, select below, both full width -
   unlike every other settings row, both the label (a raw SignalK path when
   there's no friendly name for it) and the select's own option text (a
   device name plus its bus address) are unpredictably long here, so the
   normal label-left/control-right layout wrapped inconsistently row to row
   (some labels wrapped to 2 lines, some didn't, throwing off row height and
   making text read as mismatched sizes even though it wasn't). Stacking
   sidesteps that regardless of how long either side gets. */
#duplicate-sources-container .settings-row {
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}
#duplicate-sources-container .settings-row label { flex: none; }
#duplicate-sources-container .settings-row select { width: 100%; }
.tank-settings-row .tank-label-input { flex: 1; width: auto; }
/* Same specificity gap as the Custom Service Items interval box below -
   ".tank-settings-row .tank-capacity-input" alone loses to the generic
   ".settings-row input[type=\"number\"]" rule (which also matches, since
   this row carries the settings-row class too) regardless of source
   order, so 80px silently never applied. Chaining .settings-row here
   makes this rule unambiguously the more specific one. */
.settings-row.tank-settings-row .tank-capacity-input { width: 80px; flex: none; }
.settings-hint {
  display: block;
  color: var(--label);
  font-size: 0.8rem;
  line-height: 1.45;
  padding: 0 0 12px;
}
#set-ntfy-topic { width: 200px; }
/* Live status of the Pi's alarm engine (Settings > Phone Alerts) - tabular
   digits so the "last check Ns ago" counter doesn't jitter as it refreshes. */
.alarm-engine-status { font-variant-numeric: tabular-nums; }
.alarm-engine-status.alarm-engine-bad { color: var(--red); }

@keyframes blink {
  50% { opacity: 0.3; }
}
@keyframes blink-border {
  50% { border-color: var(--red); background: var(--card-bg); }
}

/* ---------- Dark helm-display page scheme ---------- */
/* Originally the Engine screen's own hardcoded look; now a per-page choice
   any page can opt into (right-click its tab in the bottom strip) - see
   settings.pageDarkScheme and setActiveTab() in app.js. Scoped to
   body.dark-scheme so every other page's light theme is untouched. Styled
   after classic engine-room gauge displays (N2KView etc). */
body.dark-scheme { background: #0b0d10; }
body.dark-scheme #app { background: #0b0d10; min-height: calc(100vh - 61px); }
body.dark-scheme #topbar { background: #14181d; border-bottom-color: #2b3038; }
body.dark-scheme #boat-name,
body.dark-scheme #position-badge,
body.dark-scheme #clock-date,
body.dark-scheme #clock-time,
body.dark-scheme #live-label { color: var(--eng-text); }
body.dark-scheme #screen-title,
body.dark-scheme #ip-badge,
body.dark-scheme #live-dot { color: var(--eng-text-dim); }
body.dark-scheme #tab-strip-wrap { background: #14181d; border-top-color: #2b3038; }
body.dark-scheme .tab-strip-scroll-btn { color: var(--eng-text-dim); }
body.dark-scheme .tab-btn { color: var(--eng-text-dim); }
body.dark-scheme .tab-btn.active { color: #e0c23c; background: rgba(224, 194, 60, 0.14); }
body.dark-scheme .panel { background: #14181d; border-color: #2b3038; }
body.dark-scheme .panel-title { color: var(--eng-text); }
body.dark-scheme .panel.alarm { background: #2a1210; border-color: #d33a2c; }
body.dark-scheme #settings-panel { background: #14181d; color: var(--eng-text); border-left-color: #2b3038; }
body.dark-scheme .settings-section { background: #1c2127; border-color: #2b3038; }
body.dark-scheme .settings-section-title { color: var(--eng-text); border-bottom-color: #2b3038; }
body.dark-scheme .settings-cat-bar { border-bottom-color: #2b3038; }
body.dark-scheme .settings-cat-btn { background: #1c2127; border-color: #2b3038; color: var(--eng-text-dim); }
body.dark-scheme .settings-cat-btn.active { background: #e0c23c; border-color: #e0c23c; color: #14181d; }
body.dark-scheme .settings-row input,
body.dark-scheme .settings-row select { background: #14181d; border-color: #2b3038; color: var(--eng-text); }
body.dark-scheme .settings-row input[type="number"] + .unit-label { background: #14181d; border-color: #2b3038; color: var(--eng-text-dim); }
body.dark-scheme .checklist-tile-hint { color: var(--eng-text-dim); }
body.dark-scheme .btn-log-now { background: #243447; color: #7fb2e0; border: 1px solid #3a5a7a; }
body.dark-scheme .btn-log-export { background: #14181d; color: var(--eng-text); border: 1px solid #2b3038; }
body.dark-scheme .log-subview-btn { background: #14181d; color: var(--eng-text-dim); border: 1px solid #2b3038; }
body.dark-scheme .log-subview-btn.active { background: #243447; color: #7fb2e0; border-color: #3a5a7a; }

/* The engine screen specifically (not just any dark-scheme page) gets full-
   width panels (overriding the 3-track grid built for the dashboard's left/
   center/right columns) instead of being squeezed into a narrow first
   column - a structural layout default tied to being literally the Engine
   page, unrelated to the color-scheme choice above, so it stays keyed off
   screen-engine rather than dark-scheme. Only governs the one-time zoned
   layout shown before Edit Layout is ever used on this screen; once tiles
   are freely dragged (same system as the dashboard), each one is absolutely
   positioned and this stops applying to it. */
body.screen-engine .columns { display: block; }
body.screen-engine .column { width: 100%; }
#card-engineCluster { width: fit-content; margin: 0; }
#card-engineService { width: 420px; margin: 0; }
#card-hullFouling { width: 420px; margin: 0; }
#card-roundsChecklist { width: 420px; margin: 0; }
.eng-cluster { display: flex; gap: 28px; justify-content: center; flex-wrap: wrap; }
.eng-side { flex: 1; min-width: 480px; }
.eng-side-label {
  text-align: center;
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--eng-text);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 8px;
}
.eng-side-grid { display: flex; gap: 18px; align-items: flex-start; justify-content: center; }
.eng-tach-block { display: flex; flex-direction: column; align-items: center; width: 360px; flex-shrink: 0; }
.eng-tach-label {
  color: var(--eng-text-dim);
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 4px;
}
.eng-gauge-svg { display: block; width: 100%; }
.eng-tach-readout {
  margin-top: -65px;
  font-size: 2rem;
  font-weight: 800;
  color: var(--eng-text);
  font-variant-numeric: tabular-nums;
  position: relative;
  z-index: 1;
}
.eng-secondary-grid { display: grid; grid-template-columns: repeat(3, 168px); gap: 14px 19px; }
.eng-gauge-block { display: flex; flex-direction: column; align-items: center; }
.eng-gauge-label {
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--eng-text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  text-align: center;
  margin-bottom: 2px;
}
.eng-gauge-readout {
  margin-top: -41px;
  font-size: 0.92rem;
  font-weight: 800;
  color: var(--eng-text);
  font-variant-numeric: tabular-nums;
  position: relative;
  z-index: 1;
}
.eng-gauge-unit { font-size: 0.6rem; color: var(--eng-text-dim); margin-left: 2px; font-weight: 600; }
.eng-spark { width: 100%; height: 34px; margin-top: 12px; }
.eng-spark-svg { display: block; width: 100%; height: 100%; }
.eng-spark-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #6b7078;
  font-size: 0.56rem;
  font-style: italic;
  text-align: center;
}
/* ---------- Rudder angle gauge ---------- */
.rudder-gauge-wrap { display: flex; flex-direction: column; align-items: center; }
.rudder-svg { display: block; width: 100%; max-width: 340px; }
.rudder-readout { margin-top: 10px; text-align: center; position: relative; z-index: 1; }
.rudder-value { display: block; margin-bottom: 6px; font-size: 2.6rem; font-weight: 800; color: var(--eng-text); font-variant-numeric: tabular-nums; }
.rudder-side { display: block; font-size: 0.85rem; font-weight: 700; letter-spacing: 0.04em; }
/* Port/starboard keep their true red/green always - fixed literals, not
   var(--red)/var(--green), because those tokens deliberately collapse
   toward each other under night mode (see :root[data-theme="night"]) for
   ordinary alarm/status text. Port-vs-starboard is a semantic color (which
   side, not "text"), not something night mode's red-shift should touch -
   same fixed colors used for the band arcs in renderRudderGauge(). */
.rudder-side.port { color: #d33a2c; }
.rudder-side.stbd { color: #2fa84f; }
.rudder-side.midships { color: var(--eng-text-dim); }
/* ---------- Wind gauge (dark instrument-screen look, B&G/Garmin-inspired) ---------- */
.wind-gauge-bezel {
  width: 340px;
  max-width: 100%;
  background: var(--wind-bezel);
  border-radius: 22px;
  padding: 16px;
  box-shadow: inset 0 0 0 1px var(--wind-bezel-edge);
  box-sizing: border-box;
}
.wind-gauge-screen {
  background: var(--wind-screen);
  border-radius: 8px;
  padding: 14px 12px 10px;
  position: relative;
}
.wind-gauge-dial { position: relative; width: 100%; aspect-ratio: 1 / 1; }
.wind-gauge-corner { position: absolute; line-height: 1.15; }
.wind-gauge-corner .v {
  font-size: 1.75rem;
  font-weight: 800;
  color: var(--wind-white);
  font-variant-numeric: tabular-nums;
  display: block;
}
.wind-gauge-corner .l {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--wind-dim);
}
.wind-gauge-corner.tl { top: 0; left: 2px; }
.wind-gauge-corner.tr { top: 0; right: 2px; text-align: right; }
.wind-gauge-corner.bl { bottom: 0; left: 2px; }
.wind-gauge-corner.br { bottom: 0; right: 2px; text-align: right; }
.wind-gauge-legend {
  position: absolute;
  left: -2px; top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 6px;
  z-index: 2;
}
.wind-gauge-legend .dot {
  width: 20px; height: 20px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.68rem; font-weight: 800; color: var(--wind-screen);
}
.wind-gauge-readout {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
}
.wind-gauge-readout .v { font-size: 1.85rem; font-weight: 800; color: var(--wind-white); font-variant-numeric: tabular-nums; }
.wind-gauge-readout .l { font-size: 0.6rem; font-weight: 700; letter-spacing: 0.05em; color: var(--wind-dim); }
/* ---------- Pitch/roll/heave attitude indicator ---------- */
.attitude-wrap { display: flex; flex-direction: column; align-items: center; }
.attitude-svg { display: block; width: 100%; max-width: 300px; }
/* Transform is set from JS on every tick (not rebuilt), so a CSS
   transition here is what actually makes it glide between readings
   instead of snapping - see renderAttitudePanel(). */
.attitude-hull-group { transition: transform 0.22s linear; }
.attitude-heave-gauge { display: flex; justify-content: center; margin-top: 4px; }
.attitude-heave-track {
  position: relative;
  width: 10px;
  height: 70px;
  background: #1c2127;
  border: 1px solid #2b3038;
  border-radius: 5px;
}
.attitude-heave-track::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 1px;
  background: #3a4148;
}
.attitude-heave-marker {
  position: absolute;
  left: -4px;
  top: 50%;
  width: 18px;
  height: 6px;
  border-radius: 3px;
  background: var(--green);
  transform: translateY(-50%);
  transition: top 0.22s linear;
}
.eng-cruise-hint {
  margin-top: 24px;
  padding: 7px 16px;
  border-radius: 8px;
  background: rgba(41, 182, 246, 0.12);
  border: 1px solid rgba(41, 182, 246, 0.45);
  font-size: 0.98rem;
  font-weight: 700;
  color: #29b6f6;
  text-align: center;
  letter-spacing: 0.01em;
}
.eng-cruise-hint b { font-weight: 800; }
.eng-cruise-hint.gathering {
  background: transparent;
  border-color: var(--border);
  color: var(--eng-text-dim);
  font-weight: 600;
  font-style: italic;
  font-size: 0.74rem;
}
.eng-digital-row { display: flex; gap: 10px; justify-content: center; margin-top: 16px; }
.eng-digital-block {
  background: #1c2127;
  border: 1px solid #2b3038;
  border-radius: 8px;
  padding: 8px 16px;
  text-align: center;
  min-width: 100px;
}
.eng-digital-label { font-size: 0.62rem; font-weight: 700; color: var(--eng-text-dim); text-transform: uppercase; letter-spacing: 0.04em; }
.eng-digital-value { font-size: 1.4rem; font-weight: 800; color: var(--eng-text); font-variant-numeric: tabular-nums; }
.eng-digital-unit { font-size: 0.65rem; color: var(--eng-text-dim); margin-left: 3px; font-weight: 600; }
.eng-digital-sub { font-size: 0.58rem; color: var(--eng-text-dim); font-weight: 700; letter-spacing: 0.03em; margin-top: 4px; }
.eng-digital-sub b { color: var(--eng-text-dim); font-weight: 700; }
.eng-status-row { display: flex; flex-direction: column; gap: 6px; margin-top: 16px; }
.eng-status-btn {
  background: #1c2127;
  border: 1px solid #343b44;
  border-radius: 6px;
  padding: 9px 10px;
  text-align: center;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--eng-text-dim);
}
/* .on stays hardcoded white-on-red, not var(--eng-text) - this text sits on
   a solid red background for contrast, not the dark gauge background, so it
   must NOT follow night mode's red-shift (red text on a red background
   would be unreadable). */
.eng-status-btn.on { background: #d33a2c; border-color: #ff6a5a; color: #fff; }

/* ---------- Service intervals tile ---------- */
.svc-cluster { display: flex; gap: 28px; justify-content: center; flex-wrap: wrap; }
.svc-side { flex: 1; min-width: 280px; max-width: 420px; }
.svc-row {
  background: #1c2127;
  border: 1px solid #2b3038;
  border-radius: 10px;
  padding: 12px 14px;
  margin-bottom: 10px;
}
.svc-row:last-child { margin-bottom: 0; }
.svc-row-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.svc-label { font-size: 0.88rem; font-weight: 700; color: #e8e8e8; }
.svc-remaining { font-size: 0.85rem; font-weight: 800; color: #7fb2e0; font-variant-numeric: tabular-nums; }
.svc-remaining.svc-soon { color: #e0c23c; }
.svc-remaining.svc-overdue { color: #ff6a5a; }
.svc-last { font-size: 0.72rem; color: #9aa1ab; font-weight: 600; margin-bottom: 10px; }
.svc-row-bottom { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.svc-interval { display: flex; align-items: center; gap: 8px; font-size: 0.72rem; color: #9aa1ab; font-weight: 600; }
.svc-stepper { display: flex; align-items: center; gap: 6px; }
.svc-stepper button {
  width: 26px;
  height: 26px;
  background: #14181d;
  border: 1px solid #343b44;
  border-radius: 6px;
  color: #e8e8e8;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
}
.svc-stepper button:active { background: #2b3038; }
.svc-stepper-value { font-size: 0.8rem; font-weight: 700; color: #e8e8e8; font-variant-numeric: tabular-nums; min-width: 34px; text-align: center; }
.btn-svc-reset {
  background: #243447;
  border: 1px solid #3a5a7a;
  color: #7fb2e0;
  border-radius: 6px;
  padding: 7px 10px;
  font-size: 0.72rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}
.btn-svc-reset:active { background: #2f4d6b; }

/* ---------- Hull & prop fouling tile ---------- */
.hull-cluster { display: flex; gap: 28px; justify-content: center; align-items: center; flex-wrap: wrap; }
.hull-signal-wrap { flex: none; display: flex; align-items: center; }
/* Shrunk from 64px - the new .hull-profile-wrap above needed the room back
   so this tile didn't grow taller overall. */
.hull-signal-svg { width: 60px; height: auto; display: block; }
.hull-side { flex: 1; min-width: 280px; max-width: 420px; }
.hull-status {
  background: #1c2127;
  border: 1px solid #2b3038;
  border-radius: 10px;
  padding: 12px 14px;
  font-size: 0.85rem;
  font-weight: 700;
  margin-bottom: 8px;
}
.hull-status.hull-clean { color: #6fce8a; border-color: #2fa84f; }
.hull-status.hull-mild { color: #e0c23c; border-color: #6b5f2a; }
.hull-status.hull-severe { color: #ff6a5a; border-color: #6b332e; }
.hull-status.hull-gathering { color: #9aa1ab; font-weight: 600; font-style: italic; }
.hull-detail { font-size: 0.74rem; color: #9aa1ab; font-weight: 600; padding: 0 4px; margin-bottom: 4px; }
.hull-detail b { color: #e8e8e8; }
/* The title's own trailing rule line (.panel-title::after) doubles as the
   waterline for the hull profile illustration below it - no gap between
   them, so the line reads as the sea surface with the hull hanging under it. */
#card-hullFouling .panel-title { margin-bottom: 0; }
.hull-profile-wrap { margin-bottom: 0; }
.hull-profile-svg { display: block; width: 100%; height: auto; }

/* ---------- Media / stereo control ---------- */
/* Styled to read as a real marine head unit's fascia (dark anodized casing,
   backlit amber LCD, physical-looking buttons) rather than a generic form -
   deliberately always-dark and NOT wired to --card-bg/--navy/day-night,
   same reasoning as the Engine gauge cluster: a device's casing colour
   doesn't change with the page theme or time of day. The LCD text is the
   one part that DOES respond to night mode (--media-lcd/--media-lcd-dim),
   since a glowing amber readout at night is exactly the kind of thing the
   rest of this dashboard already dims to red for night vision. */
/* No internet-dependent webfont - this runs on a boat, offline has to work.
   Noto Sans Mono ships with Raspberry Pi OS by default (confirmed via
   fc-list on the Pi) and reads a lot less "typewriter" than Courier New,
   which was a big part of what made this look like a dated MP3 player. */
.media-device, .media-device * { font-family: 'Noto Sans Mono', 'DejaVu Sans Mono', ui-monospace, monospace; }

.media-device {
  width: 250px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 7px;
  background: linear-gradient(180deg, #34383c, #1a1c1f);
  border: 1px solid var(--media-lcd-glow-soft);
  border-radius: 8px;
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
  padding: 9px 10px 8px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 2px 5px rgba(0, 0, 0, 0.35),
    0 0 16px var(--media-lcd-glow-soft);
}
.media-device + .media-device { margin-top: 10px; }

.media-device-header { display: flex; align-items: center; justify-content: space-between; }
.media-device-name {
  font-weight: 700;
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #8b9096;
}
.media-power-btn {
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--media-lcd-glow-soft);
  background: linear-gradient(180deg, #3a3f44, #24272a);
  color: #7d8288;
  padding: 0;
  cursor: pointer;
}
.media-power-btn.on {
  color: var(--media-lcd);
  border-color: var(--media-lcd);
  filter: drop-shadow(0 0 3px var(--media-lcd-glow));
}
.media-power-btn:disabled { cursor: default; }

/* The "screen" - inset shadow + near-black face reads as backlit glass
   rather than a flat card. A faint scanline texture and HUD-style corner
   brackets are what actually push it from "backlit label" to "instrument
   display" - small details, but they're the difference between this and
   a flat rectangle with text in it. */
.media-lcd {
  position: relative;
  background:
    repeating-linear-gradient(rgba(255, 255, 255, 0.025) 0px, rgba(255, 255, 255, 0.025) 1px, transparent 1px, transparent 3px),
    #0a0c0a;
  border-radius: 4px;
  padding: 6px 9px 7px;
  min-height: 34px;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.7), inset 0 0 0 1px #000, 0 0 10px var(--media-lcd-glow-soft);
}
.media-lcd::before, .media-lcd::after {
  content: '';
  position: absolute;
  top: 4px;
  width: 7px; height: 7px;
  border-top: 1.5px solid var(--media-lcd-dim);
  opacity: 0.8;
}
.media-lcd::before { left: 4px; border-left: 1.5px solid var(--media-lcd-dim); }
.media-lcd::after { right: 4px; border-right: 1.5px solid var(--media-lcd-dim); }
.media-lcd-toprow { display: flex; align-items: center; justify-content: space-between; margin-bottom: 3px; }
.media-lcd-source {
  font-weight: 700;
  font-size: 0.58rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--media-lcd-dim);
}
.media-lcd-title {
  font-weight: 700;
  font-size: 0.85rem;
  line-height: 1.15;
  color: var(--media-lcd);
  text-shadow: 0 0 6px var(--media-lcd-glow);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.media-lcd-artist {
  font-weight: 500;
  font-size: 0.68rem;
  line-height: 1.2;
  color: var(--media-lcd-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 1px;
}

/* Small animated level meter - the one piece of motion on the faceplate,
   a standard "this device is alive" cue on real HUDs/head units. Static
   short bars when paused/no interactivity (demo mode), reduced-motion
   users just get the static bars permanently. */
.media-eq { display: flex; align-items: flex-end; gap: 2px; height: 9px; flex: none; }
.media-eq span { width: 2px; height: 30%; background: var(--media-lcd-dim); border-radius: 1px; }
@media (prefers-reduced-motion: no-preference) {
  .media-eq.playing span { background: var(--media-lcd); animation: media-eq-bounce 0.9s ease-in-out infinite; }
  .media-eq.playing span:nth-child(1) { animation-delay: 0s; }
  .media-eq.playing span:nth-child(2) { animation-delay: 0.2s; }
  .media-eq.playing span:nth-child(3) { animation-delay: 0.4s; }
}
.media-eq:not(.playing) span { background: var(--media-lcd-dim); }
@keyframes media-eq-bounce {
  0%, 100% { height: 25%; }
  50% { height: 100%; }
}

/* Transport (left) and zone-select tabs (right) share one compact row;
   the active zone's volume/mute/source gets its own full-width row below,
   so the native <select> has real room instead of being squeezed into
   whatever the tabs leave over - together the two rows still read as one
   continuous control surface, closer to a real fascia than a stack of
   separate form rows. */
/* Used to be space-between to spread .media-transport away from the zone
   tabs sharing this same row - now that the zone tabs are their own row
   below (see .media-zone-tabs), .media-controlbar holds just the
   transport buttons, so centering them reads as an intentional
   play/pause/skip control cluster instead of stranded on the left edge. */
.media-controlbar { display: flex; align-items: center; justify-content: center; gap: 8px; }
.media-transport { display: flex; gap: 4px; flex: none; }
.media-transport-btn {
  width: 26px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 5px;
  border: 1px solid var(--media-lcd-glow-soft);
  background: linear-gradient(180deg, #3a3f44, #24272a);
  color: #d7dadd;
  cursor: pointer;
}
.media-transport-btn:active { background: linear-gradient(180deg, #24272a, #3a3f44); }
.media-transport-btn:disabled { color: #5a5e62; cursor: default; }

/* A sibling row below .media-controlbar now, not sharing a row with the
   transport buttons - .media-device is a fixed 250px wide LCD-faceplate
   look, and a real zone name (e.g. "Desktop" - see renderMediaDevice)
   plus several zones together no longer reliably fit alongside 3 transport
   buttons on one line the way the old fixed "Z1"/"Z2" labels always did.
   flex-wrap lets extra zones spill onto additional rows instead of past
   the tile's own clip-path, which was silently hiding them. */
.media-zone-tabs { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 2px; }
.media-zone-tab {
  min-width: 20px;
  /* A real zone name read straight off the device (see renderMediaDevice)
     replaced the old fixed "Z1"/"Z2" labels - on a boat where zones have
     been renamed to something longer via the Fusion unit's own app, this
     keeps one long name from stretching the whole tab row instead of
     wrapping/growing the tile. */
  max-width: 72px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 3px 5px;
  border-radius: 4px;
  border: 1px solid var(--media-lcd-glow-soft);
  background: #202327;
  color: #7d8288;
  font-weight: 700;
  font-size: 0.58rem;
  cursor: pointer;
}
.media-zone-tab.active {
  background: var(--media-lcd);
  border-color: var(--media-lcd);
  color: #0c1f13;
  box-shadow: 0 0 8px var(--media-lcd-glow);
}

.media-zonepanels { display: flex; }
.media-zone-controls { display: flex; align-items: center; gap: 5px; width: 100%; min-width: 0; }
.media-zone-controls[hidden] { display: none; }
.media-vol-stepper {
  display: flex; align-items: center; gap: 3px;
  background: #202327;
  border: 1px solid var(--media-lcd-glow-soft);
  border-radius: 5px;
  padding: 2px 4px;
  flex: none;
}
.media-vol-btn {
  width: 17px; height: 17px;
  border-radius: 3px;
  border: none;
  background: none;
  color: #b8bdc2;
  font-size: 0.75rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}
.media-vol-btn:disabled { color: #4a4e52; cursor: default; }
.media-vol-value {
  min-width: 16px;
  text-align: center;
  font-weight: 700;
  font-size: 0.68rem;
  color: var(--media-lcd);
}
.media-mute-btn {
  width: 21px; height: 21px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 4px;
  border: 1px solid var(--media-lcd-glow-soft);
  background: #202327;
  color: #b8bdc2;
  padding: 0;
  cursor: pointer;
  flex: none;
}
.media-mute-btn.muted { background: #3a1414; border-color: #7a2e2e; color: #ff8a8a; }
.media-mute-btn:disabled { opacity: 0.5; cursor: default; }
.media-source-select {
  flex: 1;
  min-width: 0;
  background: #0a0c0a;
  border: 1px solid var(--media-lcd-glow-soft);
  color: var(--media-lcd);
  border-radius: 4px;
  padding: 3px 4px;
  font-weight: 700;
  font-size: 0.6rem;
}
.media-source-select:disabled { color: var(--media-lcd-dim); }

