/*
 * Balaton Navigátor.
 *
 * Palette is nautical — deep navy, parchment, brass — but legibility in
 * direct sunlight outranks it everywhere the two conflict. That is why the
 * navigation readout is near-white on near-black at a very large size
 * rather than parchment on navy like the rest of the chrome.
 */

:root {
  --navy-deep: #061626;
  --navy: #0b1f33;
  --navy-light: #14304a;
  --parchment: #f2e8d5;
  --parchment-dim: #b9ad97;
  --brass: #c9a24b;
  --brass-bright: #f0c661;
  --danger: #d2543f;
  --warn: #e0a33a;
  --track: #ff8a3d;

  --tap: 48px; /* minimum comfortable target with wet hands */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overscroll-behavior: none; /* no rubber-banding over the map */
  background: var(--navy-deep);
  color: var(--parchment);
  font: 16px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-text-size-adjust: 100%;
}

body {
  /* The whole app is a single screen; scrolling happens inside views only. */
  overflow: hidden;
  -webkit-user-select: none;
  user-select: none;
}

button {
  font: inherit;
  color: var(--parchment);
  background: var(--navy-light);
  border: 1px solid #24455f;
  border-radius: 10px;
  padding: 12px 10px;
  min-height: var(--tap);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Drops the legacy double-tap-to-zoom wait on iOS, so taps register at
     once instead of after a delay. */
  touch-action: manipulation;
}
button:active { background: #1d3d59; }
button.is-active { background: var(--brass); color: var(--navy-deep); border-color: var(--brass-bright); font-weight: 600; }
button:disabled { opacity: 0.4; }

/* --- Layout --------------------------------------------------------- */

#app {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bottom);
}

#header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 12px;
  background: var(--navy);
  border-bottom: 1px solid #1c3a54;
  flex: none;
}
#header h1 {
  margin: 0;
  flex: none;
  font-size: 15px;
  font-weight: 600;
  color: var(--brass);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.route-name {
  flex: 1;
  min-width: 0; /* allow the ellipsis to engage instead of overflowing */
  min-height: 0;
  padding: 4px 8px;
  font-size: 14px;
  text-align: right;
  background: none;
  border: none;
  color: var(--parchment-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Visible on both pages, because a figure that looks wrong should carry its
   own explanation. align-self overrides the header's baseline alignment,
   which a bare dot has no sensible baseline for. */
.gps-dot {
  flex: none;
  align-self: center;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #55606b;
}
.gps-dot--good { background: #4caf6d; }
.gps-dot--fair { background: var(--warn); }
.gps-dot--poor { background: var(--danger); }

/* Leaflet's own .leaflet-top/.leaflet-bottom controls are position:absolute
   at z-index 1000 — set by vendor/leaflet/leaflet.css, not by us. They are
   descendants of #map, which is position:absolute with no z-index of its
   own, so #map forms no stacking context: its Leaflet children compete
   directly inside #map-wrap's context against .datapanel and #pagedots.
   .datapanel and #pagedots are therefore raised above 1000 (below) so they
   win that contest — otherwise the zoom control paints over VMC and the
   attribution over Hátra, and since .leaflet-control keeps pointer-events,
   taps there silently zoom the invisible map underneath. `isolation:
   isolate` does not settle that internal contest; it stops these
   deliberately-raised values from escaping #map-wrap into the ROOT stacking
   context, where they would otherwise beat .view (the full-screen list
   views, a position:fixed sibling of #app at z-index 1000) and paint
   over it instead of staying underneath. */
#map-wrap { position: relative; flex: 1; min-height: 0; isolation: isolate; }
#map { position: absolute; inset: 0; background: #0a2036; }

/* Map overlay controls, stacked bottom-right. "Közép" sits lowest because it
   is the one wanted most often under way, so it falls under the thumb. */
.map-buttons {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 500;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

.map-btn {
  background: rgba(11, 31, 51, 0.92);
  border-color: var(--brass);
  color: var(--brass-bright);
  font-weight: 600;
  padding: 12px 16px;
  white-space: nowrap;
}

.add-hint {
  position: absolute;
  left: 50%;
  /* Below the page dots, which own the top of the map now. Both are centred,
     so at the old 12px they landed on each other. */
  top: 52px;
  transform: translateX(-50%);
  z-index: 500;
  background: var(--brass);
  color: var(--navy-deep);
  font-size: 14px;
  font-weight: 600;
  padding: 8px 14px;
  border-radius: 999px;
  white-space: nowrap;
  pointer-events: none;
}

/* --- Navigation readout --------------------------------------------- */

.navpanel {
  flex: none;
  background: #000;
  border-top: 1px solid #1c3a54;
  border-bottom: 1px solid #1c3a54;
  padding: 10px 12px;
  /* Vertical gestures still belong to the browser; horizontal ones are the
     page turn. Without this Chrome on Android may start its own pan and fire
     pointercancel mid-swipe, which reads as the gesture randomly not
     working. */
  touch-action: pan-y;
}

.nav-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: baseline;
  gap: 12px;
}

/* The bearing column only ever holds three digits and a degree sign, so an
   even split hands it about eighty pixels it cannot use while the distance —
   which carries a unit, and now a time beside it — runs out of room and wraps
   mid-figure. On a 390px phone `1fr 1fr` gives each column 177px; "1.24 NM
   0:18" needs roughly 208px and "105.15 km" alone needs 185px, so the wrap was
   not an edge case but the normal state. Sizing the first column to its
   content gives the second everything left over.

   Scoped to the navpanel: the data page pairs SOG with VMC, two figures of
   like kind that should stay visually parallel. */
.navpanel .nav-grid { grid-template-columns: auto 1fr; }

/* Distance and time flushed to the right edge, so the two figures sit at
   opposite ends of the panel. With both left-aligned the slack pooled at the
   right where nothing looks at it, and the two figures huddled together with
   only the 12px gap between them — which read as cramped even though there
   was room to spare. */
.navpanel .nav-grid > :last-child { text-align: right; }

/* A figure split across two lines cannot be read at a glance, which is the
   only thing this panel is for. Better to size the columns so it fits. */
.navpanel .nav-value { white-space: nowrap; }

.nav-figure { min-width: 0; }
.nav-label {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--parchment-dim);
}
/* Maximum contrast and size: this is the one thing that must be readable
   at arm's length in glare. */
.nav-value {
  font-size: 40px;
  line-height: 1.05;
  font-weight: 700;
  color: #fff;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
/* A likely header, on the tactical page. The one reading on that screen that
   asks for a decision right now, so it is the one that changes colour — and
   --warn rather than a green "go", because it is an inference about wind the
   app has no instrument for, not an instruction. */
.nav-value--warn { color: var(--warn); }

/* IDŐ, riding alongside the distance. Dimmer and smaller so the distance
   still wins the glance. */
.nav-sub {
  margin-left: 10px;
  font-size: 20px;
  font-weight: 600;
  color: var(--parchment-dim);
}
.nav-target {
  margin-top: 8px;
  font-size: 13px;
  color: var(--brass);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nav-message { font-size: 20px; font-weight: 600; color: var(--parchment); padding: 8px 0; }

/* --- Turn indicator -------------------------------------------------
 *
 * Chevrons outrank the number, which outranks the tape. That ordering is
 * the whole design: at six knots in chop the answer has to arrive before
 * anything is read, so direction and rough magnitude are carried by shape
 * and count alone. Hence no BALRA/JOBBRA labels and no numerals under the
 * tape — they would only be read by someone who already had time to read.
 *
 * No transitions anywhere here. renderNavPanel rebuilds these nodes on
 * every render, so a transition would have nothing persistent to animate.
 * Smoothing belongs in the COG damping setting, not in CSS.
 */
/* Height reserved whether or not there is a course to steer by.
 *
 * The turn readout is about 100px; the "no course yet" note is one line. Left
 * to size themselves, swapping one for the other shifted the target line, the
 * action bar and the map above by roughly 68px — and gating the course on
 * reported speed turned that from a startup one-off into something that
 * happens every time the boat slows. A panel that jumps while being read is
 * worse than either state it jumps between. */
.nav-steer-slot {
  margin-top: 12px;
  /* Exactly what the turn readout occupies: 56px of chevrons (line-height 1)
     plus the tape's 10px margin and 24px box. Measured rather than rounded
     up, because every pixel here is taken from the chart. */
  min-height: 90px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* The tactical recommendation, in the slot the chevrons occupy on the other
   two pages. Centred rather than filling the slot: it is one short sentence
   and a figure, and stretching them to 90px would leave the panel looking
   emptier than the chevrons do, not fuller.

   The chip wraps rather than truncating — "A másik csapás jobbnak tűnik" is
   the longest string here and an ellipsis in the middle of the one sentence
   that asks for a decision would be the worst possible place for one. */
.nav-rec {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-align: center;
}
.nav-rec .upwind-chip { white-space: normal; }
/* The supporting figure: the countdown behind "Layline közeledik". Smaller
   and dimmer, because the chip is what is read and this is what confirms it. */
.nav-rec__sub {
  font-size: 20px;
  font-weight: 700;
  color: var(--parchment-dim);
  font-variant-numeric: tabular-nums;
}

/* An unsteady course still shows, dimmed: mid-tack the number is real but
   not yet settled, and hiding it would make the panel jump. Also reused by
   the data page's COG and VMC figures (.nav-figure) — computeCog returns a
   non-null cog while unsteady, so without this those two would sit at full
   brightness, indistinguishable from a settled reading, while this very
   panel dims the same condition a few rows down. Same 42% for both, so
   "unsteady" reads as one convention rather than two competing ones. */
.nav-steer.is-unsteady, .nav-figure.is-unsteady { opacity: 0.42; }

.nav-turn {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 20px;
}
.nav-turn__chevrons {
  font-size: 56px;
  line-height: 1;
  font-weight: 700;
  color: var(--brass-bright);
  letter-spacing: -0.08em;
}
/* Smaller than the chevrons by a wide margin, and smaller than it used to be.
   The ordering this block is built on is chevrons over number over tape: at
   six knots in chop the direction has to arrive before anything is read, and
   the number is the follow-up question. Sizing it just under the chevrons had
   the two competing for the glance instead. Taking it down also buys the room
   the sign now needs, so "+12°" is no wider than "12°" was. */
.nav-turn__degrees {
  font-size: 32px;
  line-height: 1;
  font-weight: 700;
  color: #fff;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.nav-tape {
  position: relative;
  height: 24px;
  margin-top: 10px;
  border-bottom: 2px solid var(--navy-light);
}
.nav-tape__tick {
  position: absolute;
  bottom: 0;
  width: 1px;
  height: 6px;
  margin-left: -0.5px;
  background: var(--parchment-dim);
  opacity: 0.55;
}
.nav-tape__tick--major { height: 13px; opacity: 0.9; }

.nav-tape__marker {
  position: absolute;
  bottom: -3px;
  width: 4px;
  height: 24px;
  margin-left: -2px;
  border-radius: 2px;
  background: var(--brass-bright);
}
/* Off the end of the scale: a different colour, so a pegged marker is not
   mistaken for a reading of exactly 45 degrees. */
.nav-tape--pegged .nav-tape__marker {
  width: 8px;
  margin-left: -4px;
  background: var(--warn);
}

/* No margin: .nav-steer-slot owns the spacing now, and centres this in the
   space the turn readout would have occupied. */
.nav-cog-note {
  font-size: 16px;
  font-weight: 600;
  color: var(--parchment-dim);
}

/* --- Banners -------------------------------------------------------- */

#banners { flex: none; }
.banner {
  padding: 10px 12px;
  font-size: 14px;
  font-weight: 500;
  color: #12100a;
}
.banner--warn { background: var(--warn); }
.banner--error { background: var(--danger); color: #fff; }
.banner button {
  background: rgba(0, 0, 0, 0.2);
  border: none;
  color: inherit;
  min-height: 0;
  padding: 4px 10px;
  margin-left: 8px;
  font-size: 13px;
}

/* --- Action bar ----------------------------------------------------- */

.actions {
  flex: none;
  display: grid;
  /* minmax(0, 1fr), not 1fr: plain 1fr floors each column at its min-content
     width, so the longest label would push the bar wider than the screen and
     scroll the whole page sideways. */
  grid-template-columns: repeat(4, minmax(0, 1fr)) 52px;
  gap: 6px;
  padding: 8px;
  background: var(--navy);
}
.actions button {
  font-size: 13px;
  padding: 12px 2px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.actions button.is-recording { background: var(--danger); border-color: var(--danger); color: #fff; font-weight: 600; }

/* --- Full-screen views ---------------------------------------------- */

.view {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: var(--navy-deep);
  display: flex;
  flex-direction: column;
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bottom);
}
.view-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--navy);
  border-bottom: 1px solid #1c3a54;
  flex: none;
}
.view-header h2 { margin: 0; font-size: 17px; font-weight: 600; }
.view-header button { background: none; border: none; color: var(--brass); padding: 8px; }
.view-body {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px;
}

/* --- Lists ---------------------------------------------------------- */

.list { list-style: none; margin: 0 0 16px; padding: 0; }
.list li {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  margin-bottom: 8px;
  background: var(--navy);
  border: 1px solid #1c3a54;
  border-radius: 10px;
}
.list li.is-active { border-color: var(--brass); background: var(--navy-light); }
.list .grow { flex: 1; min-width: 0; }
.list .title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list .sub {
  font-size: 12px;
  color: var(--parchment-dim);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A whole list row acting as a button, without looking like one. */
.row-button {
  background: none;
  border: none;
  padding: 0;
  min-height: 0;
  min-width: 0;
  text-align: left;
  overflow: hidden;
}
.row-button:active { background: none; opacity: 0.6; }
.list .num {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brass);
  color: var(--navy-deep);
  font-weight: 700;
  font-size: 14px;
}
.list li.is-target .num { background: var(--brass-bright); box-shadow: 0 0 0 3px rgba(240, 198, 97, 0.3); }

/* The badge is the "make this the target" control. The visible circle stays
   32px for looks; the button around it is a full-size touch target. */
.num-button {
  flex: none;
  width: 44px;
  min-height: 44px;
  padding: 0;
  background: none;
  border: none;
  display: grid;
  place-items: center;
}
.num-button:active { background: none; }
.num-button:active .num { transform: scale(0.88); }
.num-button .num { transition: transform 0.1s ease; }

.title-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
.title-row .title { min-width: 0; }

/* Marks the active target in the list, so a start and finish sharing one
   position are still tellable apart. */
.chip {
  flex: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--navy-deep);
  background: var(--brass-bright);
  border-radius: 4px;
  padding: 2px 5px;
}
.icon-btn {
  flex: none;
  width: 44px;
  min-height: 44px;
  padding: 0;
  display: grid;
  place-items: center;
  font-size: 18px;
}
.icon-btn--danger { color: var(--danger); }

.empty { color: var(--parchment-dim); text-align: center; padding: 32px 16px; font-size: 15px; }

.section-title {
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--parchment-dim);
  margin: 20px 0 8px;
}
.section-title:first-child { margin-top: 0; }

/* --- Forms ---------------------------------------------------------- */

.field { margin-bottom: 14px; }
.field label { display: block; font-size: 13px; color: var(--parchment-dim); margin-bottom: 5px; }
.field input, .field select {
  width: 100%;
  min-height: var(--tap);
  padding: 10px 12px;
  font: inherit;
  font-size: 16px; /* below 16px iOS zooms the page on focus */
  color: var(--parchment);
  background: var(--navy);
  border: 1px solid #24455f;
  border-radius: 10px;
  -webkit-user-select: text;
  user-select: text;
}
.field .hint { font-size: 12px; color: var(--parchment-dim); margin-top: 5px; }
.row { display: flex; gap: 8px; }
.row > * { flex: 1; }
.form-error { color: var(--danger); font-size: 14px; margin-bottom: 12px; min-height: 1.2em; }
.form-warn { color: var(--warn); font-size: 13px; margin-bottom: 12px; }
.btn-primary { background: var(--brass); color: var(--navy-deep); border-color: var(--brass-bright); font-weight: 600; width: 100%; }
.note {
  font-size: 13px;
  color: var(--parchment-dim);
  background: var(--navy);
  border-left: 3px solid var(--brass);
  border-radius: 4px;
  padding: 10px 12px;
  margin: 16px 0;
  line-height: 1.5;
}

/* --- Map markers ---------------------------------------------------- */

.wp-marker {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--brass);
  color: var(--navy-deep);
  border: 2px solid var(--navy-deep);
  display: grid;
  place-items: center;
  font-weight: 700;
  font-size: 13px;
}
.wp-marker--target {
  width: 38px;
  height: 38px;
  font-size: 17px;
  background: var(--brass-bright);
  border-color: #fff;
  box-shadow: 0 0 0 3px rgba(240, 198, 97, 0.45);
}

.pos-marker {
  position: relative;
  width: 76px;
  height: 76px;
  transform-origin: center;
  /* Smooth the swing so compass jitter reads as a wobble rather than a
     twitch, but stay short enough to feel live. */
  transition: transform 0.2s linear;
}

.pos-marker__dot {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: 50%;
  background: #4da3ff;
  border: 3px solid #fff;
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.6);
}

/* The cone appears only once a heading has actually been measured. Without
   one the marker stays a plain dot, implying no direction at all. */
.pos-marker__cone {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.pos-marker--heading .pos-marker__cone { opacity: 1; }

/* The sight line continued abaft the boat, carrying the heading in figures.
   Dimmer and thinner than the line forward — it is there to tie the number
   to the axis, not to be looked along. */
.pos-marker__stub {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2px;
  height: 46px;
  margin-left: -1px;
  background: linear-gradient(to bottom, rgba(255, 45, 149, 0.75), rgba(255, 45, 149, 0.25));
  opacity: 0;
  transition: opacity 0.25s ease;
}

/* Zero-sized anchor. It rides the marker's rotation, so translateY pushes
   along whatever "astern" currently means; the chip inside is what gets
   counter-rotated back to upright. Splitting the two keeps each transform
   doing one job — combining them on a single element makes the order of
   translate and rotate load-bearing, and it silently stops being astern. */
.pos-marker__tag {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
}

.pos-marker__tag-chip {
  position: absolute;
  left: 0;
  top: 52px;
  /* Centred on that point before rotating, so the chip turns about itself
     rather than swinging around the boat. */
  margin: -11px 0 0 -26px;
  width: 52px;
  height: 22px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 11px;
  background: rgba(43, 10, 28, 0.9);
  border: 1px solid rgba(255, 45, 149, 0.7);
  color: #ffd9ec;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  opacity: 0;
  /* Matches .pos-marker's own transition exactly, so the counter-rotation
     lands on the same frames as the rotation it is cancelling. Any mismatch
     shows up as the text lolling and righting itself on every swing. */
  transition: transform 0.2s linear, opacity 0.25s ease;
}

.pos-marker--heading .pos-marker__stub,
.pos-marker--heading .pos-marker__tag-chip { opacity: 1; }

.leaflet-container { background: #0a2036; font: inherit; }
.leaflet-control-attribution {
  background: rgba(6, 22, 38, 0.82) !important;
  color: var(--parchment-dim) !important;
  font-size: 10px !important;
}
.leaflet-control-attribution a { color: var(--brass) !important; }
.leaflet-bar a {
  background: var(--navy) !important;
  color: var(--parchment) !important;
  border-color: #24455f !important;
}

.is-hidden { display: none !important; }

/* --- Data page ------------------------------------------------------ *
 *
 * Overlays the map rather than replacing it. Leaflet is never hidden, so it
 * keeps its size and needs no invalidateSize() on the way back. z-index
 * 1100 clears .map-buttons and .add-hint (both 500) as a side effect, but
 * the real target is Leaflet's own .leaflet-top/.leaflet-bottom controls at
 * 1000 (vendor/leaflet/leaflet.css): they are descendants of #map inside
 * this same stacking context (see the #map-wrap comment above), so without
 * clearing 1000 the zoom control paints over VMC and the attribution strip
 * over Hátra — and still eats the tap, silently zooming the map underneath.
 */
.datapanel {
  position: absolute;
  inset: 0;
  z-index: 1100;
  background: #000;
  /* Top padding reserves the page dots, which sit inside this same #map-wrap
     and are deliberately raised above this panel so they keep showing on both
     pages. Being above it, they do not push the figures anywhere — they land
     on top of SOG and VMC, dead centre where the two columns meet.
     8px offset + 36px of dot = 44, plus 8 to breathe. Whether it showed was
     down to how much slack space-evenly had left: clear by 27px on a 390x844,
     overlapping by 21 on a 360x640, and clear by 4 on a 360x740 — which a
     banner appearing is more than enough to close. Reserving the band settles
     it for every screen rather than for the ones that happen to be tall. */
  padding: 52px 12px 16px;
  display: flex;
  flex-direction: column;
  /* Spread down the page rather than huddled in the middle of it. The three
     rows come to about 230px; the page has 513px to put them in on a 390x844
     phone and 620px on a 430x932, so centring left roughly 280px of the panel
     dead — split between a top and a bottom margin, while the rows themselves
     sat 24px apart and read as one block of type rather than three readings.

     space-evenly rather than a bigger gap, because the surplus is not a fixed
     quantity: #map-wrap is flex:1, so a banner appearing takes its height
     straight out of this panel. Distributing whatever is left over means the
     rows open up on a tall screen and close back to the gap below on a short
     one, with no breakpoint to maintain. The gap stays as the floor — it is
     what space-evenly cannot go beneath — so the crowded case is no worse
     than it was before. Not space-between: that would pin the last row to the
     bottom edge, on top of the attribution strip. */
  justify-content: space-evenly;
  gap: 24px;
  touch-action: pan-y; /* same reason as .navpanel */
}

/* Bigger here than on the navpanel, because there is room to be. The navpanel
   shares its screen with the chart and every pixel it takes comes out of the
   map; the data page owns the whole screen and spends the surplus on size.

   Sized against the widest realistic figure rather than by eye. The binding
   case is VMC hard on the nose in km/h — "−10.1", five tabular glyphs, which
   measures 2.82em; the column it sits in is (viewport − 36px) / 2, the grid
   being two even tracks inside 12px padding and a 12px gap. That puts the
   wrap threshold at a near-constant 15.5vw across every phone width, so 15vw
   is the largest round figure that holds, with the floor and cap covering the
   ends where the ratio drifts. Nothing here says nowrap: overflowing would
   hide a digit outright, and a figure with a digit missing still looks like a
   reading. Wrapping at least looks broken. */
.datapanel .nav-value { font-size: clamp(48px, 15vw, 68px); }

/* Captions carry the meaning on this page in a way they never do on the
   navpanel, where position and shape say which figure is which. Here SOG sits
   beside VMC — two speeds, same units, both to one decimal — and the caption
   is the only thing telling them apart. At 11px in --parchment-dim that was a
   legend to be deciphered rather than read, so: parchment at full strength,
   larger, and with the tracking eased off, since letter-spacing that opens up
   small caps only smears them at this size. */
.datapanel .nav-label {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--parchment);
}

/* The subordinate row. Smaller than .nav-value because XTE and distance
   remaining are consulted, not glanced at while steering.
   Scaled for the same reason and by the same method: "◀ 0.32 NM" runs 5.1em
   — wider than any speed figure, arrow and unit included — which caps it at
   about 8.7vw. 8.2vw, floored and capped to match the row above: this is the
   tightest figure on the page, and the measurements behind these ratios come
   from SF, so the extra margin is what covers Roboto and Segoe UI. */
.data-small {
  font-size: clamp(26px, 8.2vw, 38px);
  line-height: 1.1;
  font-weight: 700;
  color: var(--parchment);
  font-variant-numeric: tabular-nums;
}

/* A 320x568 phone has not got the height for the panel at full size: three
   rows, their gaps and the reserved dot band come to more than the map area
   is, and the surplus falls off the bottom — taking the XTE row with it.
   The figures are sized against the WIDTH they must fit (see above), so no
   amount of vw arithmetic notices this; height is a separate constraint and
   needs saying separately.

   Whitespace goes first and the figures only as far as they must, so a small
   screen loses air rather than legibility. 600px rather than 640: a 360x640
   fits with 8px to spare, and there is no sense shrinking a panel that fits.

   Placed after every rule it overrides: these selectors match the ones above
   exactly, so on equal specificity it is source order alone deciding, and
   higher up the file the .data-small line here is simply dead. */
@media (max-height: 600px) {
  .datapanel { gap: 10px; padding-bottom: 4px; }
  .datapanel .nav-value { font-size: clamp(34px, 12vw, 48px); }
  .data-small { font-size: clamp(22px, 7.5vw, 30px); }
}

/* The settings colophon: app name, build, copyright.
   Deliberately the quietest thing on the screen — smaller than a hint, dim,
   centred, and separated by a rule rather than shouting for attention. It is
   reference material for a support question, not something to read. */
.settings-colophon {
  margin-top: 28px;
  padding-top: 14px;
  border-top: 1px solid var(--navy-light);
  font-size: 12px;
  text-align: center;
  color: var(--parchment-dim);
  opacity: 0.7;
}

/* --- Tactical page --------------------------------------------------- *
 *
 * Same overlay contract as .datapanel, and for the same reasons: absolute
 * over the map at z-index 1100 so Leaflet keeps its size and never needs an
 * invalidateSize, and clear of Leaflet's own controls at 1000. The 52px of
 * top padding reserves the page-dot band, which is raised above this panel
 * (z-index 1200) and would otherwise land on the first row of figures.
 * No safe-area inset here: #app already applies it, and #map-wrap sits
 * inside that, so adding it again would double-count the notch.
 */
.upwindpanel {
  position: absolute;
  inset: 0;
  z-index: 1100;
  background: #000;
  padding: 52px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  touch-action: pan-y; /* same reason as .navpanel */
  /* A hard floor under the short-screen tuning below. This panel is absolutely
     positioned over the map at z-index 1100, so anything that outgrows it does
     not merely spill — it paints across the navigation panel underneath, which
     is the one part of the screen that must stay readable. Clipping is the
     lesser failure, and it makes an overflow visible in testing instead of
     looking like a navpanel that has gone wrong. */
  overflow: hidden;
}

.upwind-figures-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 12px;
}

/* COG · SOG · LAYLINE. Three columns rather than a second 2×2 block: the 2×2
   made four figures of equal weight below the VMG pair, and four equal things
   under two equal things is a wall, not a hierarchy. */
.upwind-figures-grid--three { grid-template-columns: repeat(3, 1fr); }

/* The VMG pair is the page's reason to exist, so it is bigger here than the
   same figure is anywhere else — larger than the 40px .nav-value default, and
   the only large thing on the page now that the third row is gone. The height
   it takes comes from rows that were deleted, not from the chart. */
.upwindpanel .upwind-figures-grid .nav-value { font-size: clamp(44px, 13vw, 56px); }

/* The secondary strip. Smaller than the two VMG figures above it: those are
   the reason to be on this page, and these are the context for reading them.
   Sized like the data page's subordinate row, not like its headline. */
.upwindpanel .upwind-small .nav-value { font-size: clamp(20px, 6vw, 26px); }

/* Which mark, or why there is no answer. A fixed-height slot with the same
   copy the navpanel uses, so an unfamiliar state reads the same on either
   page. It never empties: it holds the target's name the rest of the time. */
.upwind-note {
  min-height: 18px;
  font-size: 13px;
  color: var(--brass);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chart takes whatever height the figures leave. min-height keeps it from
   collapsing to nothing on a short screen with a banner showing — a Leaflet
   container of zero height renders no tiles at all, silently. */
.upwind-map-wrap { position: relative; flex: 1; min-height: 160px; }

/* touch-action is the load-bearing rule here, not the positioning.
   vendored leaflet.css sets `touch-action: pan-x pan-y` on .leaflet-container,
   which tells the browser a horizontal pan is a native gesture it may take
   over. Mid-swipe the browser then fires pointercancel, the pager drops the
   gesture, and the page will not turn — on the phone only, where there is a
   finger to do it with. pan-y matches .upwindpanel and .navpanel: vertical
   scrolling stays native, horizontal belongs to the pager. */
#upwind-map {
  position: absolute;
  inset: 0;
  background: #04121f;
  touch-action: pan-y;
}

/* The last-tack and shift readouts, over the chart rather than in the flow.
   That is the decramping: they used to be a row of their own beneath the
   picture, which cost height on every screen for two figures that are absent
   most of the time. Absolutely positioned, an empty one moves nothing.

   pointer-events off so a chip never eats a page swipe — it sits directly on
   the surface the pager listens to. */
.upwind-map-chip {
  position: absolute;
  /* Above Leaflet's own layers. #upwind-map has no z-index of its own, so it
     creates no stacking context and its panes (400) and control container
     (1000) compete directly with anything positioned beside it. Without this
     the chips render underneath the map tiles — present in the DOM, correct
     in the accessibility tree, and invisible. */
  z-index: 1001;
  left: 8px;
  /* Clear of the attribution strip along the bottom edge. That strip is not
     ours to cover: the OSM tile usage policy requires the credit to stay
     clearly visible wherever the tiles are shown. */
  bottom: 22px;
  /* And clear of the view button in the opposite corner, so a long chip
     truncates rather than sliding underneath it. */
  max-width: calc(100% - 96px);
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(6, 22, 38, 0.82);
  color: var(--parchment-dim);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}
/* Empty chips must not draw their own pill. :empty is exact here — the chip
   is written through textContent, so "no last tack yet" really is an empty
   node rather than a space. */
.upwind-map-chip:empty { display: none; }

/* The view control sits in the bottom-RIGHT corner, clear of the two chips on
   the left and of the attribution strip along the bottom edge, which the OSM
   tile usage policy requires to stay visible.

   pointer-events on the strip itself are off, following #pagedots: only the
   button should take a tap, or this band would be an invisible ribbon across
   the bottom of the chart that swallows page swipes. */
.upwind-controls {
  position: absolute;
  z-index: 1001; /* same stacking argument as .upwind-map-chip */
  right: 8px;
  bottom: 22px;
  display: flex;
  pointer-events: none;
}

/* Full tap size even though the labels are short: these are pressed with a
   wet thumb on a heeling boat, which is the whole sizing argument for --tap. */
.upwind-button {
  pointer-events: auto;
  min-width: var(--tap);
  min-height: var(--tap);
  background: rgba(6, 22, 38, 0.85);
  color: var(--parchment);
  border: 1px solid var(--brass);
  border-radius: 8px;
  font: inherit;
  font-size: 14px;
}

/* The boat on the chart: a triangle pointing along COG, or a dot when there
   is no course to point. A divIcon rather than a canvas glyph because CSS can
   rotate it for free, and the rotation must follow COG at compass rate.

   Dimmed with .is-unsteady when the course is not settled, exactly as the
   figures are: the triangle points somewhere either way, and a confident
   arrow on an unsettled course is the lie this app avoids everywhere else. */
.upwind-boat { width: 26px; height: 26px; position: relative; }
.upwind-boat.is-unsteady { opacity: 0.42; }
/* Clipped from a full-size box rather than built from CSS borders. The border
   trick makes an element whose own box is 0x0, so `rotate()` pivots around a
   corner of nothing and the glyph swings out of the marker entirely — it
   looks like a boat pointing the wrong way, which is the one mistake this
   symbol must not make. A full-size box rotates about its centre, which is
   the boat's position, by default.

   A dart rather than a plain triangle: the notch at the stern reads as a
   direction from further away than an isoceles triangle does, and at 26px in
   sunlight that margin matters. */
.upwind-boat__arrow {
  position: absolute;
  inset: 0;
  background: #fff;
  clip-path: polygon(50% 2%, 92% 96%, 50% 74%, 8% 96%);
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
}
.upwind-boat__dot {
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
}

/* The mark's name, on the chart. Leaflet's default tooltip is a white box
   with a pointer, which reads as a popup you could dismiss; this is a label. */
.upwind-mark-label.leaflet-tooltip {
  background: rgba(6, 22, 38, 0.82);
  border: none;
  box-shadow: none;
  color: var(--brass-bright);
  font-size: 11px;
  font-weight: 700;
  padding: 1px 5px;
}
.upwind-mark-label.leaflet-tooltip::before { display: none; }

.upwind-chip {
  padding: 6px 10px;
  border-radius: 8px;
  font-size: 15px;
}
/* Staying put is the default, so it is stated quietly. Tacking is the one
   that asks for a decision, so it is the one that is filled — and in --warn
   rather than a green "go", because it is an inference, not an instruction. */
.upwind-chip--current { color: var(--brass-bright); }
.upwind-chip--opposite { background: var(--warn); color: #000; }
.upwind-chip--even, .upwind-chip--none { color: var(--parchment-dim); }
/* The "not yet" line is a note, not a verdict, so it does not take a
   verdict's weight — smaller than the three chips it stands in for. */
.upwind-chip--none { font-size: 13px; padding: 4px 0; }

/* A short screen has not got the height for this page at full size: the two
   figure rows and the chart's floor come to more than the map area is on a
   320x568, and the surplus falls off the bottom.

   The chart gives up the least here, unlike in v1. With the last-tack and
   shift readouts moved onto it as overlays there is no third row left to
   shrink, and the picture IS the page — so the figures give up a few points
   and the chart keeps its height. Same priority the data page applies at the
   same breakpoint, and placed after every rule it overrides so equal
   specificity resolves on source order. */
@media (max-height: 600px) {
  .upwindpanel { padding: 46px 12px 6px; gap: 2px; }
  /* Measured against a 320x568, which is the smallest screen this app has to
     work on: at 120px the panel outgrew #map-wrap by about forty pixels and
     the chart's bottom edge — attribution and all — landed on the navpanel's
     captions. Every value in this block was cut to buy those forty back. */
  .upwind-map-wrap { min-height: 90px; }
  .upwind-note { min-height: 16px; font-size: 12px; }
  .upwindpanel .upwind-figures-grid .nav-value { font-size: clamp(30px, 10vw, 38px); }
  .upwindpanel .upwind-small .nav-value { font-size: clamp(17px, 5vw, 22px); }
  .upwind-map-chip { font-size: 11px; }
  .upwind-chip { font-size: 13px; padding: 4px 8px; }
}

/* --- Page dots ------------------------------------------------------ *
 *
 * Overlaid on the map area rather than given a row of their own: a strip
 * under the action bar would cost the chart height on every screen, for an
 * indicator that is two dots. z-index 1200 puts them above .datapanel's
 * 1100 (itself raised above Leaflet's controls, see above), so the dots
 * keep showing on both pages instead of being buried under the data page.
 *
 * TOP centre, not bottom. The bottom strip belongs to the licence
 * attribution, which OSM's tile usage policy requires to stay clearly
 * visible on the map and explicitly forbids putting behind other UI. The
 * dots sat on top of it.
 *
 * The pill is not decoration: unlike every other overlay here the dots sit
 * over map tiles that may be pale fields or dark water, and a dim dot on a
 * light tile is no indicator at all.
 */
#pagedots {
  position: absolute;
  left: 50%;
  top: 8px;
  transform: translateX(-50%);
  z-index: 1200;
  display: flex;
  background: rgba(11, 31, 51, 0.92);
  border-radius: 999px;
  /* Only the buttons take taps, so the gap between them still pans the map. */
  pointer-events: none;
}

/* The visible dot is 8px but the target is full size. This is the only
   affordance saying a second page exists, and it has to be usable in
   gloves by someone who cannot land the swipe. */
.pagedot {
  pointer-events: auto;
  width: 44px;
  /* Shorter than --tap: this rides over the chart, and a 48px pill at the top
     of the map is a bite out of the thing you are trying to look at. 36 is
     still a target a wet thumb can find. */
  min-height: 36px;
  padding: 0;
  background: none;
  border: none;
  border-radius: 999px;
  display: grid;
  place-items: center;
}
.pagedot:active { background: none; }

/* The browser's default ring fires on a tap and then sits there, drawing a
   box around the map. Keyboard users still get it. */
.pagedot:focus { outline: none; }
.pagedot:focus-visible { outline: 2px solid var(--brass-bright); outline-offset: -3px; }

.pagedot::after {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--parchment);
  /* Against the pill rather than the map now, so this only has to read as
     "not the current page" — but it still has to be visible at all. */
  opacity: 0.5;
}
.pagedot.is-current::after { background: var(--brass-bright); opacity: 1; }
