/*
Copyright (c) 2026 SKL (Martin Abbott)

Licensed under the PolyForm Noncommercial License 1.0.0. You may use,
copy, modify, and share this software for any noncommercial purpose.
Commercial use requires a separate license from SKL.
Full text: https://polyformproject.org/licenses/noncommercial/1.0.0
See the LICENSE file at the root of this repository.
*/

/* GnodeHub dashboard (2026-09-24, "GnodeHub Dashboard Concepts 2"
   brought to life over the real backend). Linked ALONGSIDE the existing
   static/styles.css, not replacing it -- every shared modal/chrome
   component (cookie consent, roadmap popup, photo-gallery modal, claim
   modal, KYP panel, toolbar, quick-glance strip, context menu, light-
   source popup) keeps using its existing classes/rules from styles.css
   unchanged. This file only styles the dashboard-specific pieces: the
   favorite-pinned grid, the detail/overlay panels, the status-symbol
   icons, and the Edit and Add-Photo modals. Prefixed gh- (Gnode
   Hub) throughout to avoid any collision with styles.css's existing
   class names. */

/* Reclaim body's bottom gutter on this page only (2026-09-24, Martin:
   "there is plenty of space underneath [the footer] that could be
   reclaimed... eliminate the need to scroll 5-10px to get the ENTIRE
   chart on the page"). styles.css's shared `body` rule carries
   `padding: 0 1.25rem 1.25rem` -- that bottom 1.25rem sits below
   .app-footer (a flex sibling of .app-scroll-area, both inside body's
   100vh flex column), so it both shows as visible dead space under the
   copyright line AND eats directly into the height available to
   .app-scroll-area/.gh-chart-wrap, which is exactly the sliver of
   scroll Martin was seeing. Overridden here rather than in styles.css
   itself since that rule is shared by every other page (login,
   register, learn-more, multi_meter_view...) that isn't locked to the
   viewport the same way and may still want that breathing room. */
body {
  padding-bottom: 0;
}

#gh-dashboard-layout {
  display: grid;
  /* Left column stays a fixed, comfortably-readable width (device
     circles/names never need more than this to read well); the right
     column is `1fr` so widening the browser window grows the chart/
     detail side, not the device picker -- per Martin's ask: "make sure
     the full width of the screen is used (majority of it should be for
     the graph but the Gnode Selector is still large enough that its
     easily readable)". */
  grid-template-columns: 360px 1fr;
  gap: 1.5rem;
  width: 100%;
  margin: 0.75rem 0 0;
  padding: 1.25rem 1.5rem;
  box-sizing: border-box;
  /* `stretch` (was `start`) + the height below is what lets a single
     chart fill the page instead of leaving a slab of bare gradient
     underneath it (2026-09-24, Martin: "if we are only going to be
     showing a singular gnode chart... expand it vertically so there is
     minimal wasted space above/below"). .app-scroll-area (styles.css)
     is a flex child with a definite computed height, so a plain
     `height: 100%` here resolves against it with no JS measuring
     needed -- confirmed live at 1920x1080, 1024x768 and 375x812.
     `- 0.75rem` (2026-09-24, follow-up): this box also carries a
     0.75rem top margin (below) for breathing room under the toolbar.
     .app-scroll-area's overflow: auto stops that margin from collapsing
     into the scroll area the way a margin normally would, so it was
     counted as 12px of real scrollable content -- a bare `height: 100%`
     plus that margin is *always* 12px taller than the scroll area
     itself, no matter how much vertical room .gh-chart-wrap otherwise
     reclaims. Subtracting the margin here brings the box down to
     exactly match the scroll area.
     `height` + `grid-template-rows: minmax(0, 1fr)` (2026-09-24,
     second follow-up, replacing what was `min-height` with an implicit
     auto row): with several Quick Glance tags pinned, the left
     column's natural content (glance-section + the favorites grid) can
     need MORE height than this box has -- with `min-height` + an auto
     row, CSS Grid's default single-row sizing takes the max-content of
     both columns, so the taller left column dragged the WHOLE grid
     (chart column included) taller than the scroll area, bringing back
     the exact page-level scroll this box exists to prevent (confirmed
     live: 4 pinned tags forced 140px of extra height onto the whole
     page). A definite `height` + an explicit `minmax(0, 1fr)` row
     instead pins the row to this box's own height regardless of
     content, so a too-tall left column has somewhere else to go: see
     .gh-grid-panel's `min-height: 0` + `overflow-y: auto` below, which
     lets it scroll internally instead of inflating this box. Confirmed
     live with 4 fake pinned tags: page-level scroll back to 0, only
     .gh-grid-panel itself scrolls (contained to that one card). */
  height: calc(100% - 0.75rem);
  grid-template-rows: minmax(0, 1fr);
  align-items: stretch;
  background: rgba(255, 255, 255, 0.55);
  border-radius: 20px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}

/* Small-laptop / tablet-landscape: the device picker's fixed 360px
   column starts eating too much of the chart's width once the whole
   layout drops under ~1100px -- narrow it instead of letting the chart
   column get cramped. Purely a width tweak; the single-column collapse
   below (900px) fully overrides grid-template-columns anyway, so the
   two never conflict. */
@media (max-width: 1100px) {
  #gh-dashboard-layout {
    grid-template-columns: 300px 1fr;
    gap: 1.15rem;
  }
}

/* Tablet-portrait and phones, either orientation: stack the picker
   above the chart. .gh-grid itself also switches from a 3x3 grid to a
   single horizontally-scrollable row of just the tiles that actually
   exist (2026-09-24 audit: on a 375x812 phone a 3x3 grid of
   mostly-empty dashed placeholders ran ~500px tall before the chart
   ever came into view). This
   keeps the favorites one tap away without burying the actual reading,
   and the same fix also helps landscape phones (e.g. 812x375), where
   that tall a grid left almost nothing else on-screen. */
@media (max-width: 900px) {
  #gh-dashboard-layout {
    grid-template-columns: 1fr;
    /* 2026-09-24 (found testing a Note 9-sized viewport, 412x869): the
       height/grid-template-rows: minmax(0, 1fr) pair above exists so
       that on a WIDE viewport, where the picker and detail panel sit
       side by side in one row, a too-tall picker column scrolls
       internally instead of dragging the whole page taller (28e5365).
       At this single-column width the two columns instead stack into
       TWO rows -- but minmax(0, 1fr) only defines row 1, so row 2
       (the detail panel, sized `auto` to its own content -- often
       600-700px once photo/watering/stats are all in view) ate
       essentially the entire fixed container height, squeezing row 1
       (the picker: Quick Glance + favorites grid) down to a measured
       0px. Confirmed live: Quick Glance tags and the favorites grid
       were completely invisible, not just cramped. Reverted to `auto`
       height / an implicit auto row here, same as pre-28e5365 -- on
       this single-column layout there's no second column competing
       for the row, so letting each section take its natural height
       and letting .app-scroll-area's own scrollbar cover the total
       (exactly like it always has on mobile) is correct, not a
       regression to fix. */
    height: auto;
    min-height: calc(100% - 0.75rem);
    /* 2026-09-24, follow-up (Martin, Galaxy Tab S6 Lite screenshot):
       `grid-template-rows: none` left both rows as plain 'auto' tracks.
       With `align-content` defaulting to 'normal' (which computes to
       'stretch' for a grid), any extra height this box has beyond what
       its two rows actually need (see the min-height comment above --
       there often IS extra, since min-height gives this box a
       viewport-derived floor) gets split EVENLY between both auto
       tracks, not handed to whichever row can actually use it. The
       tile-grid row has nothing flexible inside it to absorb that
       extra space, so it just sat there as a dead gap above the chart
       card -- confirmed live (a Chrome DevTools trace on gnodehub.com
       at 768x1024 showed the tile-grid row inflated to 280px against
       149px of actual content, a ~132px gap accounted for by nothing
       but this stretch). `auto 1fr` fixes the row SIZES directly
       instead of relying on stretch distribution: row 1 (the tile
       grid) is sized to its own content and never over-grows, and row
       2 (the detail/chart panel) is an `fr` track that absorbs 100% of
       whatever's left -- which is exactly the "expand the chart
       vertically, minimal wasted space" behavior already intended
       elsewhere in this file, just now actually reaching this layout
       instead of half-consumed by an empty tile-grid row first. */
    grid-template-rows: auto 1fr;
    /* Matches the tile grid's own internal spacing (.gh-grid's gap is
       0.6rem) instead of inheriting the wider desktop-column gap
       (1.5rem) set on the unconditional rule above. */
    gap: 0.6rem;
  }

  .gh-grid-panel {
    /* Un-does the min-height: 0 / overflow-y: auto pair above, which
       exists only to protect the desktop side-by-side row (see the
       comment on #gh-dashboard-layout just above) -- on the stacked
       mobile layout it's not needed and was clipping this column's
       content to whatever sliver of the row survived the squeeze. */
    min-height: auto;
    overflow-y: visible;
  }

  /* 2026-09-24: the horizontal-scroll-strip override that used to live
     here (display: flex + overflow-x: auto) existed to route around the
     base grid's old fixed 3-row block on narrow screens. grid.js no
     longer sets inline grid-column/grid-row per tile, and the base
     .gh-grid rule now uses grid-auto-rows (only as many rows as there
     are tiles), so the base grid itself already lays out compactly on
     phones -- this override, and the "hide every empty tile but one"
     rule that only mattered for an always-9-cell grid, are no
     longer needed. */
}

/* Phone portrait specifically (2026-09-24, Martin: "Portrait Model
   Mobile probably shouldn't display the graphs... For Portrait mode we
   should include the Quick-Glance info first, followed by the accounts
   Favorite Gnode Card. If they rotate the screen then we can expand the
   card and display a graph"). A time-series chart is a wide-format
   thing -- narrower than this and it's fighting the format, not just
   snug -- so instead of shrinking it further, hide it in portrait and
   let a rotate to landscape (still caught by the max-width: 900px block
   above, chart included, unchanged) reveal it.
   Reordering needs .gh-grid-panel to stop being its own box: it's a
   single grid item wrapping #glance-section + #gh-grid + #gh-grid-
   overflow, so on its own there's no way to slot #gh-detail-panel (a
   separate grid item) BETWEEN glance and the tile grid using `order`
   alone -- order only reshuffles direct children of the same flex/grid
   container. `display: contents` removes .gh-grid-panel's own box
   without touching the DOM, promoting its children to direct flex items
   of #gh-dashboard-layout (now `display: flex` here) alongside
   #gh-detail-panel, so all four can be freely reordered together.
   Threshold lowered from 900px to 600px (2026-09-24, Martin: tested an
   iPad Mini -- 768x1024 in portrait -- and it was getting caught by
   this block and losing its chart, when a tablet that size has plenty
   of room for one). 600px sits comfortably between the widest realistic
   phone portrait width (~480px CSS px, even on a large phone -- these
   are CSS pixels, not physical ones) and the narrowest common tablet
   portrait width (768px, iPad Mini), so this stays phone-only without
   needing a device list. The general single-column stacking block
   above keeps its own, separate 900px threshold -- stacking Quick
   Glance/grid above the chart still makes sense on an 768px-wide
   tablet, only hiding the chart entirely does not. */
@media (max-width: 600px) and (orientation: portrait) {
  #gh-dashboard-layout {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }

  /* Specificity bump (#gh-dashboard-layout ancestor, not just the bare
     class) is required here, not cosmetic: the unconditional base rule
     further down this file (the "Favorite-pinned, column-filling 3x3
     grid" section) also targets plain `.gh-grid-panel` and sets
     `display: flex`. Two same-specificity class selectors are decided
     by source order, and that base rule happens to sit AFTER this media
     query in the file -- so without the extra ancestor it always won,
     silently cancelling `display: contents` and leaving .gh-grid-panel
     as a real flex box again. That's exactly what shipped in dba2072:
     the media query matched (verified live), the `order` values were
     all correctly assigned, but with .gh-grid-panel never actually
     becoming `contents`, #glance-section/#gh-grid/#gh-grid-overflow
     stayed trapped as ITS flex children instead of #gh-dashboard-
     layout's, so `order` had nothing to do and the page kept rendering
     in plain DOM order (glance, tile grid, overflow, THEN the detail
     card) -- confirmed on the real production build (commit dba2072,
     built 2026-09-24T17:31:53Z) via a live portrait-viewport check
     against gnodehub.com itself, not a stale-deploy issue. */
  #gh-dashboard-layout .gh-grid-panel {
    display: contents;
  }

  #glance-section { order: 1; }
  #gh-detail-panel { order: 2; }
  #gh-grid { order: 3; }
  #gh-grid-overflow { order: 4; }

  .gh-chart-controls,
  .gh-chart-wrap {
    display: none;
  }

  .gh-rotate-hint {
    display: block;
  }
}

/* Large / high-res displays: give the picker column and its tiles a
   little more room instead of leaving a fixed 360px column looking
   undersized next to an increasingly wide chart. Mirrors the existing
   1600/2560 breakpoints styles.css already uses. */
@media (min-width: 1600px) {
  #gh-dashboard-layout {
    grid-template-columns: 420px 1fr;
    gap: 2rem;
    padding: 1.5rem 2rem;
  }

  .gh-tile-photo {
    width: 66px;
    height: 66px;
  }

  .gh-grid {
    grid-auto-rows: minmax(118px, auto);
  }
}

@media (min-width: 2560px) {
  #gh-dashboard-layout {
    grid-template-columns: 480px 1fr;
    gap: 2.5rem;
  }

  .gh-tile-photo {
    width: 76px;
    height: 76px;
  }

  .gh-grid {
    grid-auto-rows: minmax(132px, auto);
  }
}

/* --- Favorite-first, sequential-fill grid -----------------------------
   2026-09-24, Martin: "Lets also scrap the idea of the profiles personal
   Favorite is stored in the middle of the 3x3 grid, instead lets keep it
   top left (Slot #1) and then additional favorited (public or owned)
   devices are listed after. This will eliminate needing to enforce a
   grid, while also allowing that section to compress down to only the
   space thats needed (which cuts the screen usage quite a bit)"

   grid.js no longer assigns any per-tile grid-column/grid-row -- tiles
   are appended in the order they should visually fill (favorite first,
   then other favorited/visible devices, then a single "+" tile), and
   this grid relies on the browser's default auto-placement (fills
   left-to-right, wrapping every 3rd column) to lay them out. Row count
   is no longer fixed at 3 (see grid-auto-rows below) -- the grid only
   ever grows as many rows as there are actual tiles. -------------------- */
.gh-grid-panel {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* 2026-09-24: #gh-dashboard-layout's row is now a definite
     minmax(0, 1fr) track (see its own comment) rather than one sized
     to whichever column has more content, so if pinned Quick Glance
     tags + the favorites grid ever need more height than the row
     gives this column, it needs somewhere to put the overflow instead
     of stretching the whole grid (and dragging the chart column along
     with it back into page-level scroll). min-height: 0 overrides the
     default "automatic minimum size" that flex/grid items get (which
     is based on content and would otherwise force this column back to
     its full content height regardless of overflow-y); overflow-y:
     auto then gives it its own contained scrollbar for the rare case
     that happens, same as .app-scroll-area already does for the page
     as a whole. Invisible in the common case -- only engages once
     content actually exceeds the available height. */
  min-height: 0;
  overflow-y: auto;
}

/* .glance-section (styles.css) carries its own top padding + bottom
   margin sized for sitting as a full-width strip at the top of the
   page. Nested here as the left column's first
   child instead, .gh-grid-panel's own `gap` already provides that
   breathing room, so zero these out rather than stacking both --
   otherwise pinning even one tag added a visibly bigger gap above the
   grid than an unpinned view has. When nothing's pinned,
   renderQuickGlance() (quick-glance.js) leaves this element completely
   empty, so it takes up no visible space beyond that shared gap. */
.gh-grid-panel .glance-section {
  padding: 0;
  margin-bottom: 0;
}

/* 2026-09-24: collapse these out of the flex flow when empty so they
   don't claim a .gh-grid-panel gap slot for nothing -- see the comment
   on the 900px .gh-dashboard-layout gap trim above for why this
   mattered enough to fix alongside it. */
#glance-section:empty,
.gh-grid-overflow:empty {
  display: none;
}

.gh-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* Was `grid-template-rows: repeat(3, minmax(104px, 1fr))`, which
     always reserved a full 3-row block regardless of tile count.
     grid-auto-rows only creates as many rows as there are tiles to
     fill, so a favorite-only view (or favorite + one or two more)
     compresses down instead of leaving empty reserved rows. */
  grid-auto-rows: minmax(104px, auto);
  gap: 0.6rem;
  background: var(--soft-gray);
  border-radius: 16px;
  padding: 0.75rem;
}

.gh-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.2rem;
  background: var(--soft-white);
  border: 2px solid transparent;
  border-radius: 12px;
  padding: 0.5rem 0.35rem;
  cursor: pointer;
  font: inherit;
  color: var(--text-dark);
  transition: border-color 0.15s ease, transform 0.1s ease;
}

.gh-tile:hover {
  transform: translateY(-1px);
}

.gh-tile.is-selected {
  border-color: var(--leaf-green);
  box-shadow: 0 0 0 2px rgba(106, 168, 79, 0.25);
}

.gh-tile-favorite-badge {
  position: absolute;
  top: 4px;
  left: 6px;
  color: var(--sun-yellow);
  filter: drop-shadow(0 0 1px rgba(0,0,0,0.5));
  font-size: 0.85rem;
}

.gh-tile-photo {
  width: 58px;
  height: 58px;
  border-radius: 50%;
  border: 3px solid #999999;
  background-color: var(--soft-gray);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a9b8a3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21V11'/%3E%3Cpath d='M12 12c0-4 3-7 7-7 0 4-3 7-7 7z'/%3E%3Cpath d='M12 14c0-3-2.5-5.5-6-5.5 0 3 2.5 5.5 6 5.5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 28px 28px;
}

.gh-tile-name {
  font-size: 0.68rem;
  font-weight: 500;
  opacity: 0.8;
  text-align: center;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gh-tile-metric {
  font-size: 0.68rem;
  opacity: 0.75;
}

.gh-grid-overflow {
  font-size: 0.8rem;
  text-align: center;
  padding: 0.25rem;
}

.gh-tile-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 2px dashed rgba(0, 0, 0, 0.14);
  border-radius: 12px;
  cursor: pointer;
  padding: 0.5rem;
  color: rgba(0, 0, 0, 0.28);
  font: inherit;
}
.gh-tile-empty:hover {
  border-color: rgba(106, 168, 79, 0.5);
  color: var(--leaf-green);
}
.gh-tile-empty-plus {
  font-size: 1.3rem;
  line-height: 1;
}

.gh-grid-empty,
.gh-grid-error {
  padding: 2rem 1rem;
  text-align: center;
  opacity: 0.7;
  grid-column: 1 / -1;
}

/* --- Detail panel ------------------------------------------------------ */
#gh-detail-panel {
  background: var(--soft-white);
  border-radius: 16px;
  padding: 1.25rem;
  min-height: 300px;
  box-sizing: border-box;
  /* Flex column (was implicit block stacking -- visually identical when
     there's no extra height to give out) so that once #gh-dashboard-
     layout actually has extra height to hand down (see its min-height +
     align-items:stretch above), .gh-detail-body below can claim it with
     flex:1 and pass it on to the chart. Shared by both things that fill
     this element -- detail-panel.js's single-plant view and
     overlay-panel.js's N-device comparison -- so "let the one chart
     fill the space" applies to both. */
  display: flex;
  flex-direction: column;
}

.gh-panel-error {
  padding: 2rem;
  text-align: center;
  opacity: 0.7;
}

.gh-detail-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.75rem;
  border-bottom: 1px solid rgba(0,0,0,0.08);
  padding-bottom: 0.6rem;
  margin-bottom: 0.75rem;
}

.gh-detail-name {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--dark-green);
}

.gh-detail-species {
  font-weight: 400;
  opacity: 0.75;
  font-size: 0.95rem;
}

.gh-detail-owner {
  font-size: 0.8rem;
  opacity: 0.6;
}

.gh-detail-status {
  font-size: 0.78rem;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  opacity: 0.75;
}

.device-status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}
.device-status-dot.device-status-fresh { background: var(--leaf-green); }
.device-status-dot.device-status-stale { background: #e8b93f; }
.device-status-dot.device-status-old { background: #c1594b; }
.device-status-dot.device-status-unknown { background: #999999; }

.gh-detail-happiness {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--dark-green);
  display: flex;
  align-items: center;
  gap: 0.15rem;
  /* Same fix as .gh-stat above, for the happiness-score pin button. */
  position: relative;
  padding-right: 0.85rem;
}

.gh-edit-btn {
  margin-left: auto;
  background: none;
  border: 1px solid var(--leaf-green);
  color: var(--dark-green);
  border-radius: 8px;
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  cursor: pointer;
}
.gh-edit-btn:hover {
  background: rgba(106, 168, 79, 0.12);
}

.gh-detail-body {
  display: grid;
  grid-template-columns: 150px 1fr;
  gap: 1.25rem;
  flex: 1 1 auto;
  min-height: 0;
}

@media (max-width: 560px) {
  .gh-detail-body {
    grid-template-columns: 1fr;
  }
}

.gh-detail-photo-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}

.gh-detail-photo {
  width: 140px;
  height: 140px;
  border-radius: 50%;
  border: 4px solid #999999;
  background-color: var(--soft-gray);
  cursor: pointer;
  padding: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a9b8a3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21V11'/%3E%3Cpath d='M12 12c0-4 3-7 7-7 0 4-3 7-7 7z'/%3E%3Cpath d='M12 14c0-3-2.5-5.5-6-5.5 0 3 2.5 5.5 6 5.5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 56px 56px;
}

.gh-photo-actions {
  display: flex;
  gap: 0.4rem;
}

.gh-icon-btn {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(0,0,0,0.12);
  background: var(--soft-white);
  color: var(--text-dark);
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gh-icon-btn.is-active {
  border-color: var(--leaf-green);
  color: var(--dark-green);
  background: rgba(106, 168, 79, 0.15);
}

.gh-watering {
  width: 100%;
  text-align: center;
}
.gh-watering-trigger {
  background: none;
  border: none;
  color: var(--dark-green);
  font-size: 0.8rem;
  cursor: pointer;
  text-decoration: underline;
}
.gh-watering-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.4rem;
  justify-content: center;
}
.gh-watering-form input[type="number"] {
  width: 70px;
}
.gh-watering-status {
  width: 100%;
  text-align: center;
  font-size: 0.72rem;
  opacity: 0.7;
}

.gh-mini-stats {
  display: flex;
  gap: 0.75rem;
  width: 100%;
  justify-content: center;
}
.gh-mini-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  font-size: 0.75rem;
  /* Same fix as .gh-stat above, for the Stage/Watered pin buttons. */
  position: relative;
}
.gh-mini-label {
  opacity: 0.55;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.gh-mini-value {
  font-weight: 600;
}

.gh-detail-main {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  min-width: 0;
  min-height: 0;
}

.gh-stat-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
}
.gh-stat {
  background: var(--soft-gray);
  border-radius: 10px;
  padding: 0.55rem 0.7rem;
  /* Anchors this tile's own Quick Glance pin button to its own corner.
     .quick-glance-tag-btn (styles.css)
     is `position: absolute`, expecting a positioned ancestor sized to
     roughly the tile itself. Without this, its nearest positioned
     ancestor is <body> (styles.css sets that), so every single pin
     button on the page -- all 4 stat tiles, both mini-stats, and the
     happiness badge -- silently piled up in the page's own top-right
     corner instead of each tile's corner. Found 2026-09-24 (Martin:
     "how do I pin items to this Quick Glance section? There used to be
     little Pin buttons"); confirmed live by injecting the real markup
     and watching the button land at the viewport's top-right corner,
     then correcting it with this one property. */
  position: relative;
}
/* 2026-09-24, gauge redesign (Martin: "Instead of presenting the raw
   numbers, lets adjust those into symbols or gauges to convey the
   idea"): the tile's small header row (label + info bubble + Quick
   Glance pin), the gauge itself, and a one-line colored status/percent
   underneath. */
.gh-stat-head {
  display: flex;
  align-items: center;
  gap: 0.2rem;
  min-height: 15px;
}
.gh-stat-label {
  font-size: 0.68rem;
  opacity: 0.6;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.gh-gauge-wrap {
  padding: 0.15rem 0;
}
/* Dial (Temp/VPD): fixed aspect ratio semicircle, centered, capped so
   two side-by-side tiles never crowd each other on a narrow phone. */
.gh-gauge-dial {
  display: block;
  width: 100%;
  max-width: 108px;
  margin: 0 auto;
}
/* Bar (Water/Light): stretches to the tile's full width -- there's no
   "too wide" concern the way a dial has, and a longer track reads its
   marker position more precisely. */
.gh-gauge-bar {
  display: block;
  width: 100%;
  height: 20px;
}
.gh-stat-substatus {
  text-align: center;
  font-weight: 700;
  font-size: 0.8rem;
}
.gh-stat-empty {
  text-align: center;
  opacity: 0.5;
  font-size: 0.85rem;
  padding: 0.6rem 0;
}
/* The hover/tap info bubble (.stat-info/.stat-info-btn/.stat-info-bubble
   base rules live in styles.css) was designed for dark stat cards -- its button
   background (rgba(255,255,255,0.18)) is nearly invisible against this
   dashboard's light .gh-stat tiles. Darker, leaf-tinted background
   instead; .stat-info-bubble itself already has its own opaque dark
   background regardless of what it sits on, so it needs no override. */
.gh-stat .stat-info-btn {
  background: rgba(0, 0, 0, 0.14);
  color: var(--text-dark);
}
.gh-stat .stat-info-btn:hover,
.gh-stat .stat-info-btn:focus-visible {
  background: rgba(0, 0, 0, 0.26);
}

/* Per-axis status symbol (ported 1:1 from the mockup): quiet when a
   reading is in its ideal band (statusIconHtml returns "" -- nothing
   renders), colored + labeled with its %-delta otherwise. */
.status-symbol {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
}
.status-symbol-delta {
  font-size: 0.7rem;
  font-weight: 700;
}

.gh-chart-controls {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.gh-metric-pill {
  border: 1px solid rgba(0,0,0,0.12);
  background: var(--soft-white);
  color: var(--text-dark);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  font-size: 0.78rem;
  cursor: pointer;
}
.gh-metric-pill.is-active {
  background: var(--leaf-green);
  border-color: var(--leaf-green);
  color: white;
  font-weight: 600;
}

.gh-chart-wrap {
  position: relative;
  height: 180px;
  /* flex-basis defers to the height above via `auto`; flex-grow lets
     this claim whatever extra vertical room .gh-detail-main ends up
     with (see the flex chain added to #gh-detail-panel/.gh-detail-body/
     .gh-detail-main above), so the one chart this page ever shows at a
     time -- single-plant or the N-device overlay -- fills the page
     instead of sitting at a fixed height with dead space below it. On
     the stacked mobile layout there's no extra room to claim, so this
     is a no-op there and it renders at the height above, same as
     before. */
  flex: 1 1 auto;
  min-height: 0;
}
.gh-chart-wrap-overlay {
  height: 240px;
}

/* Hidden by default (desktop, tablet, landscape phone all still show
   the real chart) -- only the phone-portrait media query below reveals
   this (display: block), in place of .gh-chart-controls/.gh-chart-wrap. */
.gh-rotate-hint {
  display: none;
  text-align: center;
  padding: 1rem 0.5rem;
  color: var(--text-dark);
  opacity: 0.55;
  font-size: 0.85rem;
}

/* --- Overlay (multi-select comparison) panel -------------------------- */
.gh-overlay-header {
  margin-bottom: 0.6rem;
}
.gh-overlay-title {
  font-weight: 700;
  color: var(--dark-green);
}
.gh-overlay-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.85rem;
}
.gh-overlay-chip {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--soft-gray);
  border-radius: 999px;
  padding: 0.25rem 0.5rem 0.25rem 0.35rem;
  font-size: 0.78rem;
}
.gh-overlay-chip-swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}
.gh-overlay-chip-photo {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background-color: #ddd;
  flex-shrink: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a9b8a3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21V11'/%3E%3Cpath d='M12 12c0-4 3-7 7-7 0 4-3 7-7 7z'/%3E%3Cpath d='M12 14c0-3-2.5-5.5-6-5.5 0 3 2.5 5.5 6 5.5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 12px 12px;
}
.gh-overlay-chip-name {
  font-weight: 600;
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gh-overlay-chip-value {
  opacity: 0.7;
}
.gh-overlay-chip-remove {
  border: none;
  background: none;
  color: var(--text-dark);
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0 0.15rem;
  opacity: 0.6;
}
.gh-overlay-chip-remove:hover {
  opacity: 1;
}

/* --- Generic modal dialog shell (Edit modal + Add-Photo wizard) --------
   Martin's explicit ask: the edit screen "doesn't have to be the same
   size as the Gnode tile, go ahead and open a dialog window", and photo
   upload "can be in a window" too -- same overlay + centered-panel
   pattern the existing claim-code-modal/photo-crop-modal already use in
   styles.css, just generic (any .gh-modal-body content) instead of
   one-purpose-built markup per modal. */
.gh-modal-overlay {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 2200;
  background: rgba(0, 0, 0, 0.75);
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  box-sizing: border-box;
}
.gh-modal-overlay[hidden] {
  display: none !important;
}
.gh-modal-dialog {
  position: relative;
  background: var(--soft-white);
  color: var(--text-dark);
  border-radius: 16px;
  padding: 1.75rem;
  max-width: 460px;
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
  box-sizing: border-box;
}
.gh-modal-close {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.06);
  color: var(--text-dark);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
.gh-modal-close:hover {
  background: rgba(0, 0, 0, 0.12);
}
.gh-modal-title {
  margin: 0 2rem 1rem 0;
  color: var(--dark-green);
}

/* --- Edit modal fields -------------------------------------------------- */
.gh-edit-field {
  display: block;
  margin-bottom: 0.85rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-dark);
}
.gh-edit-field-label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 0.8rem;
  font-weight: 600;
}
.gh-edit-input {
  display: block;
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.5rem 0.6rem;
  border-radius: 8px;
  border: 1px solid rgba(0,0,0,0.15);
  font-size: 0.9rem;
  box-sizing: border-box;
  font-weight: 400;
}
.gh-edit-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 1rem;
}
.gh-edit-save-btn,
.gh-wizard-next-btn,
.gh-wizard-confirm-crop-btn,
.gh-wizard-submit-btn,
.gh-guest-signup-btn {
  background: var(--leaf-green);
  color: white;
  border: none;
  border-radius: 8px;
  padding: 0.5rem 1.1rem;
  font-weight: 600;
  cursor: pointer;
}
/* .gh-guest-signup-btn is an <a>, not a <button> (it's a plain link to
   /register) -- undo link defaults so it matches the buttons it's
   grouped with above. */
.gh-guest-signup-btn {
  display: inline-block;
  text-decoration: none;
}
.gh-edit-cancel-btn,
.gh-wizard-cancel-btn,
.gh-wizard-back-btn,
.gh-guest-dismiss-btn {
  background: rgba(0,0,0,0.06);
  color: var(--text-dark);
  border: none;
  border-radius: 8px;
  padding: 0.5rem 1.1rem;
  cursor: pointer;
}
.gh-edit-status,
.gh-wizard-status {
  font-size: 0.78rem;
  opacity: 0.7;
}

/* --- Add-Photo wizard ---------------------------------------------------- */
.gh-wizard-steps {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
  font-size: 0.72rem;
}
.gh-wizard-step-dot {
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  background: rgba(0,0,0,0.06);
  opacity: 0.55;
}
.gh-wizard-step-dot.is-active {
  background: var(--leaf-green);
  color: white;
  opacity: 1;
  font-weight: 600;
}
.gh-wizard-step-dot.is-done {
  background: rgba(106, 168, 79, 0.25);
  opacity: 0.85;
}

.gh-wizard-hint {
  font-size: 0.82rem;
  opacity: 0.75;
  margin: 0 0 0.85rem 0;
}

.gh-wizard-location-opt {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.82rem;
  margin-bottom: 0.5rem;
}

.gh-wizard-file-input {
  display: block;
  margin-bottom: 0.5rem;
}

/* Reposition/zoom crop step -- identical sizing/behavior to the old
   #photo-crop-modal's viewport (160x160, matching the on-card photo's
   real render size) so what's framed here is what actually shows. */
.gh-wizard-crop-viewport {
  position: relative;
  width: 160px;
  height: 160px;
  margin: 0 auto;
  overflow: hidden;
  border-radius: 16px;
  border: 1px solid rgba(0,0,0,0.15);
  background: rgba(0,0,0,0.06);
  cursor: grab;
  touch-action: none;
}
.gh-wizard-crop-viewport:active {
  cursor: grabbing;
}
.gh-wizard-crop-img {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
  max-width: none;
  user-select: none;
  -webkit-user-drag: none;
}
.gh-wizard-crop-zoom {
  width: 100%;
  margin: 1rem 0 0.5rem 0;
}

.gh-wizard-preview {
  display: flex;
  justify-content: center;
  margin-bottom: 0.75rem;
}
.gh-wizard-preview-img {
  width: 140px;
  height: 140px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid var(--leaf-green);
}

/* No-GPS fallback: pick a city by hand (2026-09-30). */
.gh-wizard-city:empty {
  display: none;
}
.gh-wizard-city {
  margin-bottom: 0.5rem;
  font-size: 0.82rem;
}
.gh-wizard-city-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.gh-wizard-city-row .gh-edit-input {
  flex: 1;
  min-width: 0;
}
.gh-wizard-city-row .gh-wizard-city-find-btn {
  margin-top: 0.3rem;
}
.gh-wizard-city-results {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.5rem;
}
.gh-wizard-city-option {
  text-align: left;
  background: rgba(106, 168, 79, 0.12);
  color: var(--text-dark);
  border: 1px solid rgba(106, 168, 79, 0.35);
  border-radius: 8px;
  padding: 0.45rem 0.7rem;
  cursor: pointer;
}
.gh-wizard-city-option:hover,
.gh-wizard-city-option:focus-visible {
  background: rgba(106, 168, 79, 0.25);
}
.gh-wizard-city-chosen {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

/* --- Time Range custom calendar (admin only, 2026-10-07) ---------------
   Anchored under the toolbar's Time Range select. Same white card + dark
   green type + leaf-green accent as the detail panel and modals, so it
   reads as part of the dashboard rather than a bolted-on widget. Phones
   get it centered on screen instead of hanging off the toolbar's edge. */
.time-range-field {
  position: relative;
}

.range-picker-edit {
  display: inline-flex;
  align-items: center;
  margin: 0;
  padding: 0.35rem 0.45rem;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  background: transparent;
  color: var(--text-light);
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
}
.range-picker-edit:hover { background: rgba(255, 255, 255, 0.15); }
.range-picker-edit[hidden] { display: none; }

.range-picker {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 2500;
  width: 296px;
  padding: 0.85rem;
  background: var(--soft-white);
  color: var(--text-dark);
  border: 1px solid rgba(53, 94, 59, 0.18);
  border-radius: 14px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
  white-space: normal;
}
.range-picker[hidden] { display: none; }

.range-picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}
.range-picker-month {
  font-weight: 700;
  color: var(--dark-green);
}
.range-picker-nav {
  width: 2rem;
  height: 2rem;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--dark-green);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
.range-picker-nav:hover:not(:disabled) { background: var(--soft-gray); }
.range-picker-nav:disabled { opacity: 0.3; cursor: default; }

.range-picker-weekdays,
.range-picker-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.range-picker-weekdays span {
  text-align: center;
  font-size: 0.72rem;
  font-weight: 600;
  color: #6b7a6d;
  padding-bottom: 0.3rem;
}
.range-picker-grid { row-gap: 2px; }

.range-picker-day {
  /* styles.css gives every <button> padding/margins/min-width meant for
     full-size buttons -- reset so 7 days fit the card exactly. */
  width: 100%;
  min-width: 0;
  margin: 0;
  padding: 0;
  height: 2.1rem;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--text-dark);
  font-size: 0.85rem;
  cursor: pointer;
}
.range-picker-day:hover:not(:disabled):not(.is-start):not(.is-end) {
  background: var(--soft-gray);
  border-radius: 8px;
}
.range-picker-day:focus-visible {
  outline: 2px solid var(--leaf-green);
  outline-offset: -2px;
  border-radius: 8px;
}
.range-picker-day:disabled {
  color: #c4c9c4;
  cursor: default;
}
.range-picker-day.is-today {
  font-weight: 700;
  color: var(--dark-green);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.range-picker-day.in-range {
  background: rgba(106, 168, 79, 0.16);
}
.range-picker-day.is-start,
.range-picker-day.is-end {
  background: var(--leaf-green);
  color: var(--text-light);
  font-weight: 700;
}
.range-picker-day.is-start { border-radius: 8px 0 0 8px; }
.range-picker-day.is-end { border-radius: 0 8px 8px 0; }
.range-picker-day.is-start.is-end { border-radius: 8px; }

.range-picker-summary {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--soft-gray);
  font-size: 0.85rem;
  color: var(--dark-green);
  min-height: 1.2em;
}

.range-picker-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.6rem;
}
.range-picker-btn {
  padding: 0.4rem 0.9rem;
  border-radius: 8px;
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
}
.range-picker-cancel {
  border: 1px solid rgba(53, 94, 59, 0.25);
  background: var(--soft-white);
  color: var(--dark-green);
}
.range-picker-cancel:hover { background: var(--soft-gray); }
.range-picker-apply {
  border: 1px solid var(--leaf-green);
  background: var(--leaf-green);
  color: var(--text-light);
}
.range-picker-apply:hover:not(:disabled) { background: var(--dark-green); border-color: var(--dark-green); }
.range-picker-apply:disabled { opacity: 0.45; cursor: default; }

@media (max-width: 480px) {
  .range-picker {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(296px, calc(100vw - 32px));
  }
}

/* Rest (dark period) line under the Light tile (2026-10-07). Text colors
   are the darker shade of each status hue so the small label clears 4.5:1
   on the tile's light gray. */
.gh-rest {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  margin-top: 0.15rem;
  font-size: 0.75rem;
  font-weight: 600;
}
.gh-rest-icon { flex: 0 0 auto; }
.gh-rest-good    { color: #3f7a2e; }
.gh-rest-short   { color: #8a5a10; }
.gh-rest-none    { color: #a8402f; }
.gh-rest-unknown { color: #6b7a6d; }
