/* ============================================================================
   style.css  -  the skin shared by the two customer pages (login, dash)
                 on boatremote.kingjameshoward.com

   These are static files under a CSP of script-src 'self'. Nothing on either
   page may sit in a style="" attribute either, so every dynamic visual state
   is a CLASS the script toggles - never an inline style. The one exception is
   the marker's rotate(), which app.js writes through element.style; CSSOM
   writes are not policed by style-src, markup attributes are.

   Palette: a working sea palette - deep water and dusk, warm brass for the
   hardware accents, chart-paper for daylight. Both treatments are here; the
   light one is the default because this page is mostly read outdoors on a
   phone, and it is tuned for contrast in sunlight rather than for taste.
   ============================================================================ */

:root {
  color-scheme: light dark;

  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-ui: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* daylight: chart paper, ink, brass */
  --bg:          #efeade;
  --bg-sunk:     #e0d9c8;
  --card:        #fffdf7;
  --card-sunk:   #f5f1e6;
  --line:        #d8cfba;
  --line-firm:   #b9ad92;
  --ink:         #16211e;
  --ink-dim:     #55625c;
  /* faint, but never below 4.5:1 on any of the three backgrounds it lands on
     - it carries the SPEED / COURSE / HEADING labels, and this page is read
       at the helm in full sun */
  --ink-faint:   #5f6b66;
  --accent:      #8a5a10;
  --accent-soft: #f2e3c6;
  --ok:          #0d6a51;
  --ok-bg:       #d5eee4;
  --warn:        #91560a;
  --warn-bg:     #f8e6c6;
  --bad:         #a3341c;
  --bad-bg:      #f8ded5;
  --hull:        #16211e;
  --hull-edge:   #fffdf7;
  --shadow:      0 1px 2px rgba(22, 33, 30, .07), 0 6px 18px rgba(22, 33, 30, .07);
  --shadow-lift: 0 2px 4px rgba(22, 33, 30, .10), 0 14px 34px rgba(22, 33, 30, .13);
  --focus:       #0d6a51;
  --tile-dim:    grayscale(.55) brightness(1.04) opacity(.5);
}

@media (prefers-color-scheme: dark) {
  :root {
    /* dusk: deep water, not navy - the green cast is what keeps it from
       reading as the default dark-blue app background */
    --bg:          #0a1417;
    --bg-sunk:     #060e10;
    --card:        #112025;
    --card-sunk:   #0d1a1e;
    --line:        #233c43;
    --line-firm:   #35545d;
    --ink:         #e7f0ec;
    --ink-dim:     #94a9a5;
    --ink-faint:   #8fa19d;
    --accent:      #e0ad4e;
    --accent-soft: #33280f;
    --ok:          #63d5b3;
    --ok-bg:       #10362e;
    --warn:        #f0b45c;
    --warn-bg:     #382a11;
    --bad:         #ff8768;
    --bad-bg:      #3c1d15;
    --hull:        #f4efe2;
    --hull-edge:   #0a1417;
    --shadow:      0 1px 2px rgba(0, 0, 0, .4), 0 6px 18px rgba(0, 0, 0, .34);
    --shadow-lift: 0 2px 4px rgba(0, 0, 0, .45), 0 14px 34px rgba(0, 0, 0, .45);
    --focus:       #63d5b3;
    --tile-dim:    grayscale(.6) brightness(.72) opacity(.55);
  }
}

*, *::before, *::after { box-sizing: border-box; }

/* app.js hides things with the hidden property, and any author `display` rule
   below would otherwise beat the UA sheet's [hidden] and put them back on
   screen. This keeps that from being a per-selector thing to remember. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.15; }

a { color: var(--accent); }

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* numbers that change every minute must not reflow the layout around them */
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* ---------------------------------------------------------------- buttons  */

button {
  font: inherit;
  color: inherit;
  border: 0;
  background: none;
  cursor: pointer;
  touch-action: manipulation;
  white-space: nowrap;          /* a label that wraps stops looking like a button */
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5em;
  min-height: 52px;
  padding: 0 1.2em;
  border-radius: 10px;
  background: var(--ink);
  color: var(--bg);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .01em;
  transition: transform .08s ease, filter .12s ease;
}

.btn:active { transform: translateY(1px); filter: brightness(1.15); }

.btn-quiet {
  min-height: 44px;
  padding: 0 .95em;
  border: 1px solid var(--line-firm);
  border-radius: 9px;
  background: var(--card);
  color: var(--ink-dim);
  font-size: .9375rem;
  font-weight: 600;
}

.btn-quiet:active { background: var(--card-sunk); }

/* ============================================================================
   LOGIN
   ============================================================================ */

.login-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-rows: 1fr auto;
  padding: calc(24px + env(safe-area-inset-top, 0px))
           calc(20px + env(safe-area-inset-right, 0px))
           calc(20px + env(safe-area-inset-bottom, 0px))
           calc(20px + env(safe-area-inset-left, 0px));
}

.login-wrap {
  width: 100%;
  max-width: 25rem;
  margin: 0 auto;
  align-self: center;
  padding: 8px 0 32px;
}

.mark {
  display: block;
  margin: 0 auto 22px;
  width: 46px;
  height: 46px;
  color: var(--accent);
}

.login-head { text-align: center; margin-bottom: 30px; }

.wordmark {
  font-family: var(--font-display);
  font-size: 2.125rem;
  font-weight: 600;
  letter-spacing: -.012em;
  font-variation-settings: "SOFT" 20, "opsz" 40;
}

.tagline {
  margin: .45em 0 0;
  color: var(--ink-dim);
  font-size: .9375rem;
  letter-spacing: .015em;
}

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--shadow);
}

.login-card { padding: 26px 24px 28px; }

.field { margin-bottom: 18px; }

.field label {
  display: block;
  margin-bottom: 7px;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.field input {
  width: 100%;
  min-height: 52px;
  padding: 0 14px;
  border: 1px solid var(--line-firm);
  border-radius: 10px;
  background: var(--card-sunk);
  color: var(--ink);
  font: inherit;
  transition: border-color .12s ease, background .12s ease;
}

.field input:focus {
  outline: none;
  border-color: var(--focus);
  background: var(--card);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.login-card .btn { width: 100%; margin-top: 8px; }

/* the failure line is deliberately quiet - a wrong password is a typo, not an
   incident, and a red slab on a login card reads as "something is broken" */
.err {
  display: flex;
  align-items: flex-start;
  gap: .55em;
  margin: 0 0 18px;
  padding: 11px 13px;
  border-left: 3px solid var(--bad);
  border-radius: 0 8px 8px 0;
  background: var(--bad-bg);
  color: var(--bad);
  font-size: .9375rem;
  font-weight: 500;
}

.help {
  margin: 22px 0 0;
  text-align: center;
  color: var(--ink-faint);
  font-size: .875rem;
  line-height: 1.6;
}

.login-foot {
  text-align: center;
  color: var(--ink-faint);
  font-size: .8125rem;
  letter-spacing: .02em;
}

/* ============================================================================
   DASH
   ============================================================================ */

.topbar {
  position: sticky;
  top: 0;
  z-index: 500;                 /* over Leaflet panes (400) and its controls */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* viewport-fit=cover, so the bar has to keep itself out of the notch and,
     in landscape, out of the rounded corners */
  padding: calc(10px + env(safe-area-inset-top, 0px))
           calc(16px + env(safe-area-inset-right, 0px))
           10px
           calc(16px + env(safe-area-inset-left, 0px));
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

.topbar .brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -.01em;
  white-space: nowrap;          /* a wordmark does not wrap */
}

.topbar .mark-sm { width: 22px; height: 22px; color: var(--accent); flex: none; }

.fleet {
  max-width: 46rem;
  margin: 0 auto;
  padding: 16px
           calc(16px + env(safe-area-inset-right, 0px))
           calc(40px + env(safe-area-inset-bottom, 0px))
           calc(16px + env(safe-area-inset-left, 0px));
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* ---------------------------------------------------------------- boat card */

.boat {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: var(--shadow);
  overflow: hidden;
}

/* wraps rather than squeezes: on a narrow phone the pill drops below the name
   instead of crushing it into a column */
.boat-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 12px;
  padding: 16px 18px 14px;
}

.boat-name {
  font-family: var(--font-display);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -.015em;
  font-variation-settings: "SOFT" 20, "opsz" 36;
  /* break-word, not anywhere: "anywhere" also shrinks the min-content width,
     which had a flex sibling splitting "Serenity" across two lines */
  overflow-wrap: break-word;
}

.pill {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: .45em;
  padding: 7px 12px;
  border-radius: 999px;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  white-space: nowrap;
}

.pill::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.pill.is-ok    { background: var(--ok-bg);   color: var(--ok); }
.pill.is-warn  { background: var(--warn-bg); color: var(--warn); }
.pill.is-bad   { background: var(--bad-bg);  color: var(--bad); }

/* ---------------------------------------------------------------- the map   */

/* isolate: leaflet.css puts its control corners at z-index 1000, which without
   a stacking context here would paint them straight over the sticky topbar */
.map-wrap {
  position: relative;
  z-index: 0;
  isolation: isolate;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--card-sunk);
}

.map {
  height: 56vh;
  height: 56dvh;
  min-height: 300px;
  max-height: 560px;
  background: var(--card-sunk);
}

/* the track: styled here rather than through Leaflet's colour options so it
   follows the palette into dark mode. CSS beats SVG presentation attributes,
   which is what Leaflet writes. */
.track-line {
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  opacity: .8;
}

/* no fix, or nothing heard for a while: the chart is history, so it is shown
   as history. The controls and the marker stay at full strength - only the
   tiles fade, so the page never looks broken, just quiet. */
.map-wrap.is-dim .leaflet-tile-pane { filter: var(--tile-dim); }
.map-wrap.is-dim .track-line { opacity: .35; }

/* never reported a position at all: there is no chart worth showing, and an
   empty grey Leaflet pane looks like a failure rather than an absence */
.map-wrap.no-pos .map,
.map-wrap.no-pos .recenter { display: none; }
.map-wrap.no-pos { border-bottom: 0; }

/* Shown only after the reader drags the chart away from the boat. Bottom
   LEFT: the attribution sits bottom right and OSM's terms require it to stay
   legible, so the one control that appears over the map keeps clear of it. */
.recenter {
  position: absolute;
  left: 12px;
  bottom: 14px;
  z-index: 1100;                /* over leaflet's own 1000-level corners */
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-lift);
}

.no-pos-note {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 180px;
  padding: 28px 24px;
  text-align: center;
  color: var(--ink-dim);
  font-size: .9375rem;
}

/* -------------------------------------------------------------- status strip */

.strip { padding: 15px 18px 6px; }

.updated {
  display: flex;
  align-items: baseline;
  gap: .4em;
  font-size: 1.0625rem;
  font-weight: 600;
}

.updated .lead { color: var(--ink-dim); font-weight: 500; }

/* the one sentence that explains an unusual state in plain words */
.explain {
  margin: 11px 0 0;
  padding: 12px 14px;
  border-left: 3px solid currentColor;
  border-radius: 0 9px 9px 0;
  font-size: .9375rem;
  line-height: 1.55;
}

.explain.is-warn { background: var(--warn-bg); color: var(--warn); }
.explain.is-bad  { background: var(--bad-bg);  color: var(--bad); }
.explain strong  { font-weight: 700; }
.explain .calm   { display: block; margin-top: .35em; color: var(--ink-dim); font-weight: 500; }

.stats {
  display: grid;
  /* minmax(0,...) not 1fr: a plain 1fr floors at min-content, and the widest
     reading ("119.0°T") was making its column wider than the other two */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  margin: 15px 0 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--line);
  overflow: hidden;
}

.stat { background: var(--card); padding: 12px 6px 13px; text-align: center; }

.stat dt {
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* fluid: three readings across a 320 px phone is the tightest this gets, and
   "119.0°T" must not spill out of its tile there */
.stat dd {
  margin: 4px 0 0;
  font-size: clamp(1.125rem, 6vw, 1.5rem);
  font-weight: 600;
  letter-spacing: -.01em;
  white-space: nowrap;
}

.stat dd .unit {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--ink-dim);
  margin-left: .12em;
}

/* an unknown speed is "—", not "— kn": the unit would imply a reading */
.stat.is-void dd { color: var(--ink-faint); font-weight: 500; }
.stat.is-void .unit { display: none; }

.mode-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

.mode-row dt {
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.mode-row dd { margin: 0; font-size: 1.0625rem; font-weight: 600; }

/* ------------------------------------------------------- position, copyable */

.pos {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  width: 100%;
  min-height: 60px;
  margin: 14px 0 18px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card-sunk);
  text-align: left;
}

.pos:active { background: var(--accent-soft); }

/* selectable so that "Hold to copy" - what the button falls back to when the
   clipboard API is refused - is advice the reader can actually act on */
.pos-val {
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1.35;
  -webkit-user-select: text;
  user-select: text;
}

.pos-hint {
  flex: none;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.pos.is-copied .pos-hint { color: var(--ok); }
.pos.is-void { opacity: .6; }

/* ------------------------------------------------------------ boat marker   */

.boat-mark { color: var(--hull); }

.boat-mark-rot {
  width: 34px;
  height: 34px;
  transform-origin: 50% 50%;
  will-change: transform;
}

.boat-mark svg { display: block; width: 34px; height: 34px; }
.boat-mark .hull { fill: currentColor; stroke: var(--hull-edge); stroke-width: 2.2; }

/* heading and course both unknown: point nowhere rather than point wrongly */
.boat-mark.no-dir .hull { display: none; }
.boat-mark.no-dir .dot  { display: block; }
.boat-mark .dot { display: none; fill: currentColor; stroke: var(--hull-edge); stroke-width: 2.2; }

.boat-mark .halo { fill: var(--ok); opacity: .22; }
.boat-mark.is-stale .halo { display: none; }
.boat-mark.is-stale { color: var(--ink-faint); }

@media (prefers-reduced-motion: no-preference) {
  .boat-mark:not(.is-stale) .halo {
    animation: ping 2.6s ease-out infinite;
    transform-origin: 50% 50%;
  }
}

@keyframes ping {
  0%   { transform: scale(.55); opacity: .35; }
  70%  { transform: scale(1);   opacity: 0; }
  100% { transform: scale(1);   opacity: 0; }
}

/* ------------------------------------------------------------ leaflet trims */

.leaflet-container {
  font: inherit;
  background: var(--card-sunk);
}

/* Leaflet's default 26px zoom buttons are below a usable touch target */
.leaflet-touch .leaflet-bar a {
  width: 40px;
  height: 40px;
  line-height: 40px;
  font-size: 22px;
  background: var(--card);
  color: var(--ink);
  border-bottom-color: var(--line);
}

.leaflet-touch .leaflet-bar { border: 1px solid var(--line-firm); box-shadow: var(--shadow); }
.leaflet-touch .leaflet-bar a:hover { background: var(--card-sunk); color: var(--ink); }

.leaflet-control-attribution {
  background: var(--card);
  color: var(--ink-faint);
  font-size: 10px;
}

.leaflet-control-attribution a { color: var(--ink-dim); }

/* --------------------------------------------------------- transient notes  */

/* "your phone cannot reach the server" is a different fact from "the boat is
   not reporting", and conflating the two would tell the owner his boat went
   silent every time he walks into a lift */
.netnote {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 900;
  max-width: 30rem;
  margin: 0 auto;
  padding: 13px 16px;
  border: 1px solid var(--line-firm);
  border-radius: 12px;
  background: var(--card);
  box-shadow: var(--shadow-lift);
  color: var(--ink-dim);
  font-size: .9375rem;
  text-align: center;
}

.loading {
  padding: 60px 20px;
  text-align: center;
  color: var(--ink-faint);
}

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

/* ---------------------------------------------------------------- wider     */

@media (min-width: 34rem) {
  body { font-size: 17px; }
  .fleet { padding: 22px 22px 48px; gap: 22px; }
  .boat-name { font-size: 2rem; }
  .stat dd { font-size: 1.75rem; }
  .map { height: 60vh; height: 60dvh; max-height: 620px; }
}
