/* ============================================================
   Vessel Tracker — visual system

   Subject: a small boat crossing open water, watched from shore by
   people who love the crew. The page's one job: "where are they now,
   and where have they been." The live position is the hero; the wake
   behind it is history.

   Palette pulled from nautical charts, not the usual dashboard defaults:
     --deep    #0b2b3a  chart-sea ink (panel, text)
     --hull    #123b4f  slightly lifted sea
     --paper   #eae3d2  weathered chart paper (panel ground)
     --buoy    #e4572e  buoy/port-hand red — the live position marker
     --wake    #2e6f8e  the vessel's trail
     --brass   #c9a227  brasswork accent, used sparingly
   ============================================================ */

:root {
  --deep: #0b2b3a;
  --hull: #123b4f;
  --paper: #eae3d2;
  --paper-dim: #d8cfb8;
  --buoy: #e4572e;
  --wake: #2e6f8e;
  --brass: #c9a227;

  --panel-w: 380px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--deep);
  background: var(--deep);
}

#map {
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* ---- Side panel: a brass-edged chart plate over the map ---- */
/* Sits top-right; the MapLibre navigation control is placed top-left. */
#panel {
  position: absolute;
  z-index: 1000;
  top: 16px;
  right: 16px;
  width: var(--panel-w);
  max-width: calc(100vw - 32px);
  background: var(--paper);
  border: 1px solid var(--paper-dim);
  border-radius: 4px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
  padding: 18px 20px 14px;
}

.panel-head {
  margin-bottom: 16px;
}

.panel-head h1 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: center;
}

/* Total voyage distance, a quiet line under the vessel name. Tracks the
   date-range view, so it reads as "miles shown" not a fixed odometer. */
.vessel-miles {
  margin-top: 3px;
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--hull);
  letter-spacing: 0.02em;
  text-align: center;
}

.vessel-miles[hidden] { display: none; }

/* ---- Live position readout: the hero of the panel ---- */
.readout {
  padding: 12px 0 14px;
  border-top: 1px solid var(--paper-dim);
  border-bottom: 1px solid var(--paper-dim);
}

.readout-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--hull);
  opacity: 0.75;
  margin-bottom: 6px;
}

.readout-coords {
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--deep);
  /* Never break a coordinate mid-value across lines. */
  white-space: nowrap;
}

.readout-time {
  margin-top: 4px;
  font-size: 0.8rem;
  color: var(--hull);
}

/* The age of the fix, appended to the label. Normal case and no letter-spacing
   so it reads as data rather than part of the heading. */
.readout-age {
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.72rem;
}

.readout-age::before {
  /* NBSP after the separator — CSS trims a trailing plain space in content. */
  content: " \00b7\00a0";
}

/* Nothing to append before the first fix lands. */
.readout-age:empty::before {
  content: none;
}

/* ---- Tracking server error ---- */
/* Sits above the title: the first thing read, since nothing below it can be
   trusted to be current while it's showing. */
.server-error {
  margin-bottom: 12px;
  padding: 7px 10px;
  border-radius: 3px;
  border: 1px solid var(--buoy);
  background: rgba(228, 87, 46, 0.12);
  color: var(--buoy);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-align: center;
}

.server-error[hidden] { display: none; }

/* ---- Mobile collapse handle ---- */
/* Desktop never collapses, so the handle doesn't exist there. */
.panel-handle {
  display: none;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- Environment readings ---- */
/* No border-top here — .readout's border-bottom already closes that section. */
.env:not(:empty) {
  margin: 14px 0 12px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 16px;
}

.env-item {
  display: flex;
  flex-direction: column;
}

.env-item .label {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hull);
  opacity: 0.75;
}

.env-item .value {
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 0.95rem;
  font-weight: 600;
}

/* ---- Foot / status line ---- */
.panel-foot {
  font-size: 0.72rem;
  color: var(--hull);
  opacity: 0.8;
  display: flex;
  align-items: center;
  gap: 7px;
}

.panel-foot::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--brass);
  flex: none;
}

.panel-foot.live::before { background: #3a9d5d; }
.panel-foot.stale::before { background: var(--buoy); }

/* ---- Map marker: the live position, a pulsing buoy ---- */
/* The only marker not drawn on the GL canvas — MapLibre positions this DOM
   element over the map. It needs no pointer-events guard: logged points are
   hit-tested by querying rendered features at the cursor, not by DOM z-order,
   so the buoy cannot swallow a click meant for the point beneath it. */
.vessel-dot {
  width: 16px;
  height: 16px;
  background: var(--buoy);
  border: 2px solid var(--paper);
  border-radius: 50%;
  box-shadow: 0 0 0 rgba(228, 87, 46, 0.6);
  animation: pulse 2.4s ease-out infinite;
}

@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(228, 87, 46, 0.5); }
  70%  { box-shadow: 0 0 0 16px rgba(228, 87, 46, 0); }
  100% { box-shadow: 0 0 0 0 rgba(228, 87, 46, 0); }
}

/* ---- Point popups: a small chart-paper plate at the fix ---- */
.maplibregl-popup-content {
  background: var(--paper);
  color: var(--deep);
  border-radius: 3px;
  border-top: 2px solid var(--brass);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
  padding: 10px 12px;
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* The tip is an SVG-ish wedge per anchor position; colour them all. */
.maplibregl-popup-anchor-top .maplibregl-popup-tip { border-bottom-color: var(--brass); }
.maplibregl-popup-anchor-top-left .maplibregl-popup-tip { border-bottom-color: var(--brass); }
.maplibregl-popup-anchor-top-right .maplibregl-popup-tip { border-bottom-color: var(--brass); }
.maplibregl-popup-anchor-bottom .maplibregl-popup-tip { border-top-color: var(--paper); }
.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip { border-top-color: var(--paper); }
.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip { border-top-color: var(--paper); }
.maplibregl-popup-anchor-left .maplibregl-popup-tip { border-right-color: var(--paper); }
.maplibregl-popup-anchor-right .maplibregl-popup-tip { border-left-color: var(--paper); }

.maplibregl-popup-close-button {
  color: var(--hull);
  font-size: 1.1rem;
  padding: 0 5px;
}

.maplibregl-popup-close-button:hover {
  background: transparent;
  color: var(--deep);
}

.point-popup {
  /* Keep the dual local/UTC timestamp on one line. */
  min-width: 280px;
}

.pp-when {
  font-size: 0.7rem;
  color: var(--hull);
  opacity: 0.75;
  white-space: nowrap;
}

.pp-where {
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 0.95rem;
  font-weight: 600;
  margin-top: 3px;
  white-space: nowrap;
}

.pp-env {
  display: grid;
  grid-template-columns: auto auto;
  gap: 2px 12px;
  margin: 8px 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--paper-dim);
}

.pp-env dt {
  font-size: 0.7rem;
  color: var(--hull);
  opacity: 0.8;
}

.pp-env dd {
  margin: 0;
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 0.78rem;
  font-weight: 600;
  text-align: right;
}

/* ---- Log popup: a written entry, not a fix ---- */
/* Wider and softer than a point popup — this one has prose in it. */
.log-popup {
  max-width: 300px;
}

.lp-head {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--brass);
}

.lp-when {
  margin-top: 2px;
  font-size: 0.72rem;
  color: var(--hull);
  opacity: 0.8;
}

.lp-where {
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 0.9rem;
  font-weight: 600;
  margin-top: 3px;
  white-space: nowrap;
  color: var(--deep);
}

.lp-text {
  margin: 8px 0 0;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--deep);
  /* The boat's line breaks are the author's — keep them. */
  white-space: pre-wrap;
}

.lp-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
}

.lp-photos img {
  width: 88px;
  height: 88px;
  object-fit: cover;
  border-radius: 3px;
  border: 1px solid var(--paper-dim);
}

.lp-foot {
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--paper-dim);
  font-size: 0.68rem;
  color: var(--hull);
  opacity: 0.8;
}

/* ---- Responsive: panel docks to the bottom on narrow + medium screens ---- */
/* Up to 1200px gets the docked card. The desktop top bar needs real horizontal
   room; on a 13" tablet (~1024–1200px) it runs out and wraps badly, so tablets
   get the clean docked layout too. Only genuinely wide screens get the bar. */
@media (max-width: 1200px) {
  #panel {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: 100%;
    max-width: 100%;
    border-radius: 12px 12px 0 0;
    border-left: none;
    border-right: none;
    border-bottom: none;
    /* Room for the handle above the title. */
    padding-top: 8px;
    /* Never taller than the viewport; the body scrolls if the track has a lot
       of env readings. */
    max-height: 85vh;
    overflow-y: auto;
  }

  .panel-handle {
    display: block;
    width: 100%;
    /* A big target — the grip is small but the hit area is not. */
    padding: 6px 0 10px;
    background: transparent;
    border: none;
    cursor: pointer;
  }

  .panel-handle-grip {
    display: block;
    width: 40px;
    height: 4px;
    margin: 0 auto;
    border-radius: 2px;
    background: var(--paper-dim);
  }

  .panel-head {
    /* Tighter than desktop — vertical space is the scarce thing here. */
    margin-bottom: 10px;
  }

  .panel-head h1 {
    font-size: 0.85rem;
  }

  /* Collapsed: only title + position + status remain. */
  #panel.collapsed .panel-body {
    display: none;
  }

  #panel.collapsed .readout {
    border-bottom: none;
    padding-bottom: 4px;
  }
}

/* ---- Wide desktop: the panel is a full-width top bar, not a floating card ---- */
/* A big floating box over open water was in the way. On wide screens (>1200px)
   the same elements lay out as a bar spanning the top edge: name + position +
   range on row 1, every env reading on row 2 (wrapping if there are a lot).
   At 1200px and below the docked card above is used instead — the bar needs the
   width. */
@media (min-width: 1201px) {
  #panel {
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    max-width: 100%;
    border: none;
    border-bottom: 1px solid var(--paper-dim);
    border-radius: 0;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
    padding: 10px 20px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 24px;
    row-gap: 6px;
  }

  /* The wrapper vanishes from layout so .env and .range become direct flex
     children of the bar — .env can then be forced onto its own row while
     .range rides up on row 1. The element stays in the DOM so the mobile
     collapse (which toggles .panel-body) still works below the breakpoint. */
  .panel-body {
    display: contents;
  }

  /* Vessel name: an inline heading at the far left, not a centered title. */
  .panel-head {
    margin-bottom: 0;
  }

  .panel-head h1 {
    font-size: 1rem;
    white-space: nowrap;
  }

  /* Centered under the title, matching the mobile card. panel-head shrinks to
     its content on the bar, so centering lands the miles under the name. */
  .vessel-miles {
    margin-top: 2px;
    font-size: 0.72rem;
  }

  /* Position readout stays the hero — big mono coords — but loses the card
     dividers. Grid places the hero coordinate alone on the left spanning both
     rows, with the caption above the timestamp in a column to its right:

       [ coordinate ]  label
       [ coordinate ]  time

     Mobile is untouched — .readout stays a plain block there, so these grid
     rules simply don't apply below the breakpoint. */
  .readout {
    padding: 0;
    border: none;
    display: grid;
    grid-template-columns: auto auto;
    grid-template-rows: auto auto;
    align-items: center;
    column-gap: 14px;
    row-gap: 0;
  }

  .readout-coords {
    grid-column: 1;
    grid-row: 1 / span 2;
    font-size: 1.25rem;
    align-self: center;
  }

  .readout-label {
    grid-column: 2;
    grid-row: 1;
    margin-bottom: 0;
    white-space: nowrap;
    align-self: end;
  }

  .readout-time {
    grid-column: 2;
    grid-row: 2;
    margin-top: 0;
    font-size: 0.72rem;
    white-space: nowrap;
    align-self: start;
  }

  /* Row 2: every reading, horizontal, wrapping. flex-basis:100% forces the
     line break so readings always start a fresh row under row 1. */
  .env:not(:empty) {
    order: 10;
    flex-basis: 100%;
    margin: 6px 0 0;
    padding-top: 8px;
    border-top: 1px solid var(--paper-dim);
    display: flex;
    flex-wrap: wrap;
    gap: 6px 28px;
  }

  /* Each reading reads as "Label value" on one line rather than stacked. */
  .env-item {
    flex-direction: row;
    align-items: baseline;
    gap: 6px;
  }

  /* Range control rides up to row 1, pushed to the right. Label inline with
     the inputs instead of stacked above them. Scoped under #panel so these win
     over the base .range rules regardless of source order — the base block is
     declared after this media query, and at equal specificity the later rule
     would otherwise reassert its border-top/padding/margin (that was the stray
     line under the date range). */
  #panel .range {
    order: 5;
    margin: 0 0 0 auto;
    padding: 0;
    border: none;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  #panel .range-label {
    margin-bottom: 0;
    white-space: nowrap;
  }

  #panel .range-inputs {
    flex: none;
  }

  #panel .range-inputs input[type="date"] {
    flex: none;
    width: auto;
  }

  /* Reset drops its top margin and full width — it's an inline button here,
     sitting after the inputs rather than stacked below them. */
  #panel .range-reset {
    margin-top: 0;
    width: auto;
    white-space: nowrap;
  }

  /* Server-error pill sits inline at the far left of row 1 when it fires,
     rather than as a full-width banner. */
  .server-error {
    order: -1;
    margin-bottom: 0;
  }

  /* Both map control stacks are pinned to the top; push them below the bar so
     it neither covers the zoom buttons (top-left) nor lets a control edge show
     through under the bar (top-right). The offset is set in JS from the bar's
     measured height (syncControlOffset) — a hardcoded value here can't track a
     bar whose height changes with env rows / name length / viewport. */
}

/* ---- Date range: a footer control below the status line ---- */
/* Sits at the very bottom of the panel so the live position stays the hero.
   Reads as a readout first (it shows the track's real span until touched)
   and a control second. */
.range {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--paper-dim);
}

.range-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--hull);
  opacity: 0.75;
  margin-bottom: 6px;
}

.range-inputs {
  display: flex;
  align-items: center;
  gap: 8px;
}

.range-inputs input[type="date"] {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
  font-size: 0.78rem;
  color: var(--deep);
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid var(--paper-dim);
  border-radius: 3px;
  padding: 4px 6px;
}

.range-sep {
  color: var(--hull);
  opacity: 0.6;
  font-size: 0.8rem;
  flex: none;
}

.range-reset {
  margin-top: 8px;
  width: 100%;
  font-family: inherit;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hull);
  background: transparent;
  border: 1px solid var(--paper-dim);
  border-radius: 3px;
  padding: 5px;
  cursor: pointer;
}

.range-reset:hover {
  border-color: var(--brass);
  color: var(--deep);
}

/* ---- Export: an icon on the date-range row ---- */
/* Rare action, so it gets no visual weight at rest — it reads as part of the
   range control rather than a call to action. */
.export-icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Matches the date inputs' height so the row stays level. */
  width: 28px;
  height: 28px;
  padding: 0;
  color: var(--hull);
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid var(--paper-dim);
  border-radius: 3px;
  cursor: pointer;
}

.export-icon:hover {
  color: var(--deep);
  border-color: var(--brass);
  background: rgba(255, 255, 255, 0.85);
}

/* display: inline-flex above would otherwise defeat the hidden attribute. */
.export-icon[hidden] { display: none; }

/* ---- Export format picker ---- */
/* Above the panel (z 1000) and MapLibre's controls. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}

.modal[hidden] { display: none; }

.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(11, 43, 58, 0.55);
}

.modal-card {
  position: relative;
  width: 100%;
  max-width: 400px;
  background: var(--paper);
  border-top: 2px solid var(--brass);
  border-radius: 4px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
  padding: 18px 20px 16px;
}

.modal-title {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.modal-sub {
  margin: 4px 0 14px;
  font-size: 0.72rem;
  color: var(--hull);
  opacity: 0.8;
}

.modal-formats {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.fmt {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
  font-family: inherit;
  background: rgba(255, 255, 255, 0.45);
  border: 1px solid var(--paper-dim);
  border-radius: 3px;
  padding: 9px 11px;
  cursor: pointer;
}

.fmt:hover {
  border-color: var(--brass);
  background: rgba(255, 255, 255, 0.75);
}

.fmt-name {
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--deep);
}

.fmt-note {
  font-size: 0.68rem;
  color: var(--hull);
  opacity: 0.8;
  line-height: 1.35;
}

.modal-cancel {
  margin-top: 12px;
  width: 100%;
  font-family: inherit;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hull);
  background: transparent;
  border: 1px solid var(--paper-dim);
  border-radius: 3px;
  padding: 6px;
  cursor: pointer;
}

.modal-cancel:hover {
  border-color: var(--brass);
  color: var(--deep);
}

/* ---- Log reader modal ---- */
/* Shares the .modal base (centered, backdrop, z 2000) but the card is a roomy
   readable column that scrolls, rather than a compact picker. A long log fills
   it and scrolls internally instead of growing off the map. */
.log-modal-card {
  max-width: 700px;
  width: 70vw;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  padding: 0;
}

/* The close sits over the scrolling body, pinned to the card's top-right. */
.log-modal-close {
  position: absolute;
  top: 6px;
  right: 8px;
  z-index: 1;
  width: 30px;
  height: 30px;
  padding: 0;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--hull);
  background: var(--paper);
  border: 1px solid var(--paper-dim);
  border-radius: 3px;
  cursor: pointer;
}

.log-modal-close:hover {
  border-color: var(--brass);
  color: var(--deep);
}

.log-modal-body {
  overflow-y: auto;
  padding: 18px 20px 16px;
}

/* The log content is the same .lp-* DOM as the inline popup, but here it has
   room to breathe: drop the popup's max-width cap and enlarge the photos. */
.log-modal-body .log-popup {
  max-width: none;
}

.log-modal-body .lp-photos img {
  width: 140px;
  height: 140px;
}

/* Mobile: fill the screen. A tap on the backdrop, the ×, or Escape returns to
   the map. */
@media (max-width: 560px) {
  .log-modal {
    padding: 0;
  }

  .log-modal-card {
    width: 100%;
    max-width: 100%;
    height: 100%;
    max-height: 100%;
    border-radius: 0;
  }
}

/* ---- Photo lightbox ---- */
/* A photo from the log modal, full-size over a dark scrim. Above the modal
   (z 2000) so it stacks on top; closing it returns to the modal. */
.lp-photo--clickable {
  cursor: zoom-in;
}

.photo-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 3000;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(11, 43, 58, 0.9);
  cursor: zoom-out;
}

.photo-overlay.open {
  display: flex;
}

.photo-overlay-img {
  max-width: 95%;
  max-height: 95%;
  object-fit: contain;
  border-radius: 3px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}

/* ---- Repo tab: a small floating GitHub link back to the project ---- */
/* Bottom-left corner of the map, logo only. Low z-index so the docked mobile
   panel (which expands from the bottom) always wins if the two ever overlap.
   Zoom controls are top-left, so the bottom-left corner is clear. */
.repo-tab {
  position: absolute;
  z-index: 500;
  bottom: 12px;
  left: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  color: var(--paper);
  background: rgba(11, 43, 58, 0.82);
  border: 1px solid rgba(234, 227, 210, 0.25);
  border-radius: 50%;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(2px);
}

.repo-tab:hover {
  color: #fff;
  border-color: var(--brass);
}

.repo-tab svg {
  flex: none;
  fill: currentColor;
}

/* ---- Accessibility floors ---- */
:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .vessel-dot { animation: none; }
}
