/*
 * Sopptur - interface.
 *
 * The design has one governing concern: one-handed use, in the rain, on a
 * phone. That has concrete consequences:
 *
 *  - Anything tappable is at least 48 px tall. That is the WCAG minimum, and
 *    roughly a fingertip in gloves.
 *  - The primary action sits at the bottom of the screen, within thumb reach,
 *    not at the top where a desktop menu would live.
 *  - Contrast is high. The screen is read in daylight under a canopy.
 *  - No animations that delay an action.
 */

:root {
  --bg: #10130f;
  --surface: #1b201a;
  --surface-raised: #262d24;
  --border: #3a4436;
  --text: #eef2ec;
  --text-muted: #a8b3a3;

  --accent: #7cb342;          /* own observations, primary action */
  --accent-strong: #8bc34a;
  --danger: #d94f2b;
  --warning: #d9a62b;
  --imported: #4a90d9;        /* imported occurrences - clearly another colour */

  --radius: 12px;
  --touch: 48px;
  /* The bar at the foot of a phone. Named because anything raised above it —
     a sheet, a backdrop's inset — has to be positioned against it, and those
     are `position: fixed` and cannot ask the bar how tall it is. */
  --bottomnav-h: 57px;
  /* The collapsed credit strip at the foot of the map. Named because the find
     button, the walk recorder and the legend all have to sit clear of it. */
  --attribution-h: 21px;
  --gap: 12px;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f5f7f3;
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --border: #d3dbcd;
    --text: #161a14;
    --text-muted: #5a6455;
    --accent: #4a7c1f;
    --accent-strong: #3d6a19;
  }
}

* { box-sizing: border-box; }

/*
 * `hidden` must beat a display set by a class.
 *
 * The attribute's own rule lives in the user-agent stylesheet, so any class
 * in this file outranks it. Several things here are laid out with `display`
 * on the element itself and hidden by JavaScript setting `el.hidden` — and
 * every one of those was hidden in the DOM and drawn on the screen. The
 * install offer shipped that way for exactly as long as it took to look at
 * it: shown on every page, to every browser, whether or not the app could be
 * installed at all.
 *
 * Nothing here shows an element by writing `style.display`, so making the
 * attribute win costs nothing.
 */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;              /* Below 16 px, iOS zooms in on focus. */
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
}

body {
  display: flex;
  flex-direction: column;
  /* Accounts for the notch and home indicator on modern phones. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

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

/* ------------------------------------------------------------------- topbar */

/*
 * Title on the left, destinations on the right — until they stop fitting.
 *
 * Four destinations and a page title do not fit across a 320 px phone, and
 * the row used to resolve that by shrinking the title away to nothing: the
 * narrowest screens got no page heading at all and a nav clipped at the right
 * edge. Wrapping is the better answer. Below the width where both fit, the
 * nav drops onto its own full-width row and every destination keeps a
 * finger-sized target.
 */
.topbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px var(--gap);
  padding: 8px 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  min-height: var(--touch);
  flex: 0 0 auto;
}

.topbar__title {
  font-size: 1.05rem;
  font-weight: 600;
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Two words, one of which is a button.
 *
 * The language you are in is a label; the one you are not is what you press.
 * A list to open and choose from would be more machinery than a choice
 * between two deserves — and a dropdown is what is being taken out of this
 * interface rather than added to it. */
.langswitch {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  margin: 0;
}

.langswitch__current,
.langswitch button {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 44px;
  padding: 0 6px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  font-family: inherit;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
}

.langswitch__current {
  color: var(--text);
  background: var(--surface-raised);
}

.langswitch button {
  color: var(--text-muted);
  cursor: pointer;
}

.topbar__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  /* Grows to fill its own row once wrapped, and is never squeezed narrower
     than its labels: a destination that has to be aimed at is worse than one
     that costs a row of height. */
  flex: 1 1 auto;
  justify-content: flex-end;
}

.topbar__nav a {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44 px is the floor for something a wet thumb has to hit. */
  min-height: var(--touch);
  padding: 0 12px;
  border-radius: var(--radius);
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.9rem;
  white-space: nowrap;
}

.topbar__nav a[aria-current="page"] {
  background: var(--surface-raised);
  color: var(--text);
}

/* ---------------------------------------------------------------- bottom bar */

/*
 * The destinations, at the foot of the screen.
 *
 * This is where a phone keeps them, and it is the half of the screen a thumb
 * reaches without shifting grip — which is the whole premise of the rest of
 * this file. Along the top they were both out of reach and, once there were
 * four of them, too wide: the row was clipped at 320 px and the page title had
 * been squeezed away to nothing to make room.
 *
 * A flex sibling of <main>, not a fixed overlay. The body is already a column,
 * so this shortens the map rather than covering its bottom edge, and every
 * overlay the map anchors down there keeps its own position with nothing to
 * re-layer.
 */
.bottomnav {
  display: flex;
  flex: 0 0 auto;
  background: var(--surface);
  border-top: 1px solid var(--border);
  /* Findable in sunlight without being looked for. */
  box-shadow: 0 -2px 14px rgb(0 0 0 / 22%);
  z-index: 20;
}

.bottomnav a {
  flex: 1 1 0;
  min-width: 0;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;

  /* Taller than the 48 px floor: this is the control used with one cold hand
     while walking, and the whole column belongs to it. */
  min-height: 56px;
  padding: 6px 2px;

  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.66rem;
  font-weight: 500;
  line-height: 1.2;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
}

/* A control in the bar rather than a destination: same shape, because to a
   thumb it is the same kind of thing, and a button that looked different
   would be asking to be read before it could be pressed. */
.bottomnav__action {
  flex: 1 1 0;
  min-width: 0;

  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;

  min-height: 56px;
  padding: 6px 2px;

  border: 0;
  background: none;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 0.66rem;
  font-weight: 500;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* A drawing with its name under it, rather than five words in a row: it is
   what every other app on the phone already puts here, so it needs no
   learning. The label stays — an icon alone is a guess. */
.bottomnav__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 26px;
  border-radius: 13px;
}

.bottomnav__icon svg {
  width: 21px;
  height: 21px;
  display: block;
}

/* Where you are, said with a shape as well as a colour. Under a canopy, in
   the rain, a hue on its own does not carry. */
.bottomnav .current {
  color: var(--text);
  font-weight: 700;
}

.bottomnav__action.current { color: var(--text); font-weight: 700; }

.bottomnav .current .bottomnav__icon {
  /* Mixed from the accent rather than taken from --surface-raised, which is
     the same white as --surface in the light theme: the pill was there in the
     dark and invisible in daylight, which is the half that matters here. */
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--accent-strong);
}


/* The dimmed map behind a raised sheet. Tapping it is the way back down. */
.sheet-backdrop {
  position: fixed;
  inset: 0;
  /* Under the bar, over everything else. The bar stays lit while a sheet is
     up, so the entry that raised it keeps its marking and the other
     destinations remain one tap away rather than two. */
  z-index: 15;
  background: rgb(0 0 0 / 45%);
}

/* On a pointer the panel is a tray under its own button and never came up
   from anywhere, so it has neither a grip nor anything to dim. */
.layer-panel__grip { display: none; }

@media (min-width: 700px) {
  .sheet-backdrop { display: none; }
}

/* ------------------------------------------------------------------ content */

main {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Scrolling stops at the ends of the page instead of chaining to the
     document behind it. That is what stops a list dragged past its top from
     triggering the browser's pull-to-refresh, which in a standalone window
     reloads the whole app — and does it just as a thumb flicks a list of
     finds, which is the gesture this application is made of. */
  overscroll-behavior-y: contain;
}

main.main--map {
  overflow: hidden;               /* The map handles its own panning. */
  position: relative;
}

.content {
  padding: var(--gap);
  max-width: 720px;
  margin: 0 auto;
}

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

#map {
  position: absolute;
  inset: 0;
}

/*
 * One line of credit, expanding on tap.
 *
 * There are fourteen sources and every one of them is a licence requirement.
 * At three or four lines they covered the foot of the map and ran underneath
 * the find button — in the way and unreadable at the same time. One line, and
 * the rest a tap away.
 *
 * Every element anchored to the bottom of the map clears this strip, which is
 * why its height is named rather than repeated.
 */
.map-attribution {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 4px 8px;
  font-size: 0.58rem;
  line-height: 1.3;
  color: var(--text-muted);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  max-height: var(--attribution-h);
  overflow: hidden;
  cursor: pointer;
  z-index: 2;
}

.map-attribution__mark {
  flex: 0 0 auto;
  font-weight: 700;
}

.map-attribution__text {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Says there is more, and which way it goes. Without it the strip reads as a
   line that happens to be cut off rather than as something openable. */
.map-attribution__more {
  flex: 0 0 auto;
  font-weight: 700;
}

.map-attribution__more::before { content: "▲"; }

.map-attribution[data-expanded="true"] {
  align-items: flex-start;
  max-height: 45vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.map-attribution[data-expanded="true"] .map-attribution__text {
  white-space: normal;
}

.map-attribution[data-expanded="true"] .map-attribution__more::before { content: "▼"; }

/* The layer switcher sits top right - out of the thumb's way. */
.layer-toggle {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 3;
}

.layer-panel {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 4;
  width: min(300px, calc(100vw - 24px));
  max-height: 70vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}

.layer-panel h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin: 12px 4px 4px;
}

.layer-panel h2:first-child { margin-top: 4px; }

.layer-option {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 4px 8px;
  border-radius: 8px;
  cursor: pointer;
}

.layer-option:hover { background: var(--surface-raised); }

.layer-option input { width: 20px; height: 20px; flex: 0 0 auto; }

.layer-option__meta {
  display: block;
  font-size: 0.7rem;
  color: var(--text-muted);
}

/* Marking of non-commercial sources, so it is visible in the menu and not
   only in a report. */
.badge-nc {
  display: inline-block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--warning);
  color: #1a1400;
}

/* A bar for a model weight.
 *
 * The relative size of the components is the thing to take away, and a column
 * of percentages does not show it. Drawn rather than written for the same
 * reason the map is a map.
 */

.weight-bar {
  height: 6px;
  margin-top: 3px;
  border-radius: 3px;
  background: var(--surface-raised);
  overflow: hidden;
}

.weight-bar span {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 3px;
}

/* Fields that are one tap away rather than on screen. */

.more-fields {
  margin-top: 4px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

.more-fields summary {
  cursor: pointer;
  list-style: none;
  min-height: 40px;
  display: flex;
  align-items: center;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent-strong);
  touch-action: manipulation;
}

.more-fields summary::-webkit-details-marker { display: none; }

.more-fields summary::after {
  content: " ▾";
  margin-left: 6px;
  transition: transform 120ms ease;
}

.more-fields[open] summary::after { transform: rotate(180deg); }

/* --------------------------------------------------- species information
 *
 * Collapsed by default. Seven species with six weighted components each is a
 * page nobody scrolls through; the name and a one-line summary are what a
 * reader wants, and the numbers are there when they want to argue.
 */

.species-detail summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 2px 0;
}

.species-detail summary::-webkit-details-marker { display: none; }

/* A disclosure arrow that turns, drawn rather than relying on the browser's
   default marker - which differs between them and cannot be positioned. */
.species-detail summary::before {
  content: "▸";
  position: absolute;
  right: var(--gap);
  color: var(--text-muted);
  transition: transform 120ms ease;
}

.species-detail[open] summary::before { transform: rotate(90deg); }

.species-detail { position: relative; }

.species-detail__name {
  font-weight: 600;
  font-size: 1rem;
  padding-right: 20px;
}

.species-detail__summary {
  font-size: 0.78rem;
  color: var(--text-muted);
}

.species-detail__heading {
  font-size: 0.8rem;
  margin: 14px 0 6px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.species-detail__facts {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  font-size: 0.82rem;
}

.species-detail__facts dd { margin: 0; }

/* ---------------------------------------------------------- species rows
 *
 * One row per mushroom with two switches: where it could grow, and whether
 * the weather is right. Fourteen checkboxes is a list nobody reads.
 */

.species-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 46px;
  padding: 6px 8px;
  border-radius: 8px;
}

.species-row:hover { background: var(--surface-raised); }

.species-row__name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.85rem;
  line-height: 1.25;
}

/* The forecast line sits under the name on its own row. Without `block` it
   runs straight on from the species name - "Blek piggsoppbest Saturday" -
   which is how it read until this rule existed. */
.species-row__outlook {
  display: block;
  margin-top: 2px;
  font-size: 0.68rem;
  font-weight: 400;
  color: var(--text-muted);
}

.species-row__toggles {
  display: flex;
  gap: 10px;
  flex: 0 0 auto;
  align-items: center;
}

/* A real switch rather than a button: two independent states side by side
   read better as switches, and a pressed button is easy to misread as the
   thing that is *about* to happen. */
.species-switch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  cursor: pointer;
  touch-action: manipulation;
}

.species-switch__label {
  font-size: 0.6rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.species-switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.species-switch__track {
  position: relative;
  display: block;
  width: 38px;
  height: 22px;
  border-radius: 11px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  transition: background 120ms ease, border-color 120ms ease;
}

.species-switch__track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text-muted);
  transition: transform 120ms ease, background 120ms ease;
}

.species-switch input:checked + .species-switch__track {
  background: var(--accent);
  border-color: var(--accent);
}

.species-switch input:checked + .species-switch__track::after {
  transform: translateX(16px);
  background: #0d1408;
}

/* The hotspot switch takes the red of its own layer. */
.species-switch[data-role="hotspot"] input:checked + .species-switch__track {
  background: #ba1c1c;
  border-color: #ba1c1c;
}

.species-switch[data-role="hotspot"] input:checked + .species-switch__track::after {
  background: #ffe8e8;
}

/* The soppvær switch takes the warm colour of the layer it draws, so the
   control and its effect are recognisably the same thing. */
.species-switch[data-role="conditions"] input:checked + .species-switch__track {
  background: #e8763a;
  border-color: #e8763a;
}

.species-switch[data-role="conditions"] input:checked + .species-switch__track::after {
  background: #1a0d05;
}

/* Keyboard focus has to remain visible: the real input is off-screen. */
.species-switch input:focus-visible + .species-switch__track {
  outline: 2px solid var(--accent-strong);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ legend
 *
 * Sits opposite the layer button, above the attribution. Shown only while a
 * modelled surface is visible, because it is the only thing on the map whose
 * colour needs explaining.
 */

.map-legend {
  position: absolute;
  left: 12px;
  bottom: calc(12px + var(--attribution-h));
  z-index: 3;
  width: min(230px, calc(100vw - 24px));
  padding: 10px 12px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  border: 1px solid var(--border);
  font-size: 0.75rem;
}

.map-legend__title {
  font-weight: 600;
  margin-bottom: 5px;
  font-size: 0.72rem;
}

/* Keys stack when more than one surface is on, with a rule between them so
   they do not read as one continuous scale. */
.map-legend__section + .map-legend__section {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.map-legend__caveat {
  margin: 8px 0 0;
  padding-top: 6px;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.66rem;
  line-height: 1.3;
}

/* The habitat ramp: one hue getting darker. Kept in step by hand with
   RAMP in pipeline/tiles.py. */
.map-legend__ramp {
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(
    to right,
    rgba(120, 150, 120, 0.25),
    rgba(90, 140, 80, 0.55),
    rgba(45, 105, 55, 0.8),
    rgba(18, 68, 38, 0.92)
  );
}

/* The conditions layer has its own ramp, and the legend has to follow it: a
   green key over an orange surface would be worse than no key. Kept in step
   with CONDITIONS_RAMP in the same file. */
/* Hotspots: only the top of the scale is drawn at all, so the key shows a
   hard edge rather than a gradient. Kept in step with HOTSPOT_RAMP. */
.map-legend__section[data-kind="hotspot"] .map-legend__ramp {
  background: linear-gradient(
    to right,
    rgba(200, 60, 50, 0) 0%,
    rgba(200, 60, 50, 0) 60%,
    rgba(232, 90, 70, 0.45) 66%,
    rgba(186, 28, 28, 0.85) 85%,
    rgba(150, 12, 12, 0.95) 100%
  );
}

.map-legend__section[data-kind="conditions"] .map-legend__ramp {
  background: linear-gradient(
    to right,
    rgba(255, 200, 80, 0.25),
    rgba(247, 160, 45, 0.55),
    rgba(232, 110, 35, 0.75),
    rgba(170, 30, 25, 0.92)
  );
}

.map-legend__scale {
  display: flex;
  justify-content: space-between;
  color: var(--text-muted);
  font-size: 0.68rem;
  margin-top: 3px;
}

.map-legend__note {
  margin: 8px 0 0;
  color: var(--text-muted);
  font-size: 0.68rem;
  line-height: 1.35;
}

/* ------------------------------------------------------- the primary action */

.fab__plus {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
}

.fab {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  /*
   * Shrink to the words, not to half the screen.
   *
   * An absolutely positioned box with `left: 50%` and no width gets only the
   * space from there to the right edge to fit in — half the viewport — and
   * shrink-to-fit then wraps whatever does not. The primary action of the
   * whole application has been reading «Record a / find» on two lines on
   * every phone since it was written, at 160 px wide on a 320 px screen.
   */
  width: max-content;
  max-width: calc(100% - 24px);
  /* No safe-area term. <main> now stops above the bottom bar, which itself
     sits above the body's safe-area padding, so adding the inset here would
     count the home indicator twice and float the button clear of the map. */
  bottom: calc(20px + var(--attribution-h));
  z-index: 5;

  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;

  min-height: 60px;
  padding: 0 28px;
  border: none;
  border-radius: 30px;

  background: var(--accent);
  color: #0d1408;
  font-size: 1.1rem;
  font-weight: 700;
  text-decoration: none;

  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
  /* Removes the 300 ms tap delay in older mobile browsers. */
  touch-action: manipulation;
}

.fab:active { background: var(--accent-strong); transform: translateX(-50%) scale(0.98); }

.fab[disabled] { opacity: 0.55; }

/* ------------------------------------------------------------------ position */

.gps-status {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-radius: 20px;
  background: color-mix(in srgb, var(--surface) 90%, transparent);
  border: 1px solid var(--border);
  font-size: 0.78rem;
  max-width: calc(100vw - 90px);
}

.gps-status__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--text-muted);
  flex: 0 0 auto;
}

.gps-status[data-quality="good"] .gps-status__dot { background: var(--accent); }
.gps-status[data-quality="fair"] .gps-status__dot { background: var(--warning); }
.gps-status[data-quality="poor"] .gps-status__dot { background: var(--danger); }

/* -------------------------------------------------------------------- forms */

form { margin: 0; }

.field { margin-bottom: 16px; }

.field label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-muted);
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="datetime-local"],
select,
textarea {
  width: 100%;
  min-height: var(--touch);
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

textarea { min-height: 88px; resize: vertical; }

input[type="file"] {
  width: 100%;
  padding: 12px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

.field__help { font-size: 0.78rem; color: var(--text-muted); margin-top: 4px; }
.field__error { font-size: 0.82rem; color: var(--danger); margin-top: 4px; }

.button-row { display: flex; gap: var(--gap); flex-wrap: wrap; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--touch);
  padding: 0 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
}

.btn--primary { background: var(--accent); border-color: var(--accent); color: #0d1408; }
.btn--danger { background: transparent; border-color: var(--danger); color: var(--danger); }
.btn--wide { flex: 1 1 160px; }
.btn--icon { min-width: var(--touch); padding: 0 12px; }

/* -------------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
  margin-bottom: var(--gap);
}

.card h2 { margin-top: 0; font-size: 1.05rem; }

.obs-list { list-style: none; margin: 0; padding: 0; }

.obs-item {
  display: flex;
  gap: var(--gap);
  align-items: center;
  padding: 10px;
  min-height: 64px;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}

.obs-item:last-child { border-bottom: none; }

.obs-item__thumb {
  width: 56px;
  height: 56px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--surface-raised);
  flex: 0 0 auto;
}

.obs-item__body { flex: 1 1 auto; min-width: 0; }
.obs-item__title { font-weight: 600; }
.obs-item__meta { font-size: 0.8rem; color: var(--text-muted); }

.tag {
  display: inline-block;
  font-size: 0.7rem;
  padding: 2px 8px;
  border-radius: 10px;
  background: var(--surface-raised);
  color: var(--text-muted);
}

.tag--unidentified { background: var(--warning); color: #1a1400; }

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 8px;
}

.photo-grid img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 8px;
}

.photo-grid figure { margin: 0; position: relative; }

/* ----------------------------------------------------------------- messages */

.messages { list-style: none; margin: 0; padding: 0; }

.messages li {
  padding: 12px var(--gap);
  border-left: 4px solid var(--accent);
  background: var(--surface-raised);
  margin-bottom: 4px;
}

.messages li.warning { border-left-color: var(--warning); }
.messages li.error { border-left-color: var(--danger); }

/* Queue status for offline submission (phase 3). */
.queue-banner {
  display: none;
  align-items: center;
  gap: 10px;
  padding: 10px var(--gap);
  background: var(--warning);
  color: #1a1400;
  font-size: 0.85rem;
  font-weight: 600;
}

.queue-banner[data-pending="true"] { display: flex; }

/* The offer to install. Quieter than the queue banner above it, which is
   about work at risk; this one is only a convenience and must not read as an
   alarm the first time the app is opened. */
.install-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px var(--gap);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  font-size: 0.82rem;
}

.install-banner__text {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted);
}

.install-banner__dismiss {
  flex: 0 0 auto;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}

.empty-state { text-align: center; padding: 48px 16px; color: var(--text-muted); }

/* MapLibre's own controls adapted for finger use. */
.maplibregl-ctrl-group button { width: 40px; height: 40px; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* --------------------------------------------------------------- popups
 *
 * MapLibre ships its own popup styling, which paints a white background and
 * then lets the text colour inherit from the page. On a dark theme that means
 * near-white text on white: invisible. Nothing here is decoration - the popup
 * was unreadable without it.
 */

.maplibregl-popup-content {
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 6px 24px rgb(0 0 0 / 35%);
  padding: 10px 12px;
  font-family: var(--font);
  font-size: 0.85rem;
  line-height: 1.45;
}

.maplibregl-popup-content a {
  color: var(--accent-strong);
}

/* The tip is a bordered triangle drawn as a CSS border, so it takes the
   background colour on whichever side it points from. All four have to be set
   or the popup grows a white beak on three of them. */
.maplibregl-popup-anchor-top .maplibregl-popup-tip,
.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,
.maplibregl-popup-anchor-top-right .maplibregl-popup-tip {
  border-bottom-color: var(--surface-raised);
}

.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,
.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {
  border-top-color: var(--surface-raised);
}

.maplibregl-popup-anchor-left .maplibregl-popup-tip {
  border-right-color: var(--surface-raised);
}

.maplibregl-popup-anchor-right .maplibregl-popup-tip {
  border-left-color: var(--surface-raised);
}

.maplibregl-popup-close-button {
  color: var(--text-muted);
  font-size: 1.1rem;
  padding: 2px 7px;
}

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

/* ------------------------------------------------------------ hotspot pins */

.pin-popup__title {
  font-weight: 600;
  display: block;
}

.pin-popup__rank {
  color: var(--danger);
  font-variant-numeric: tabular-nums;
}

.pin-popup__meta {
  color: var(--text-muted);
  font-size: 0.78rem;
}

/* ------------------------------------------------------ worth going now
 *
 * The list form of the hotspot layer. A bottom sheet rather than a side
 * panel: it is read one-handed, and the bottom of the screen is the part of
 * a phone a thumb reaches.
 */

.hotspot-panel {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 8px;
  z-index: 6;
  max-height: 42vh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 -4px 24px rgb(0 0 0 / 35%);
  padding: 8px 10px 10px;
}

.hotspot-panel[hidden] {
  display: none;
}

.hotspot-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 6px;
}

.hotspot-panel__close {
  background: none;
  border: 0;
  color: var(--text-muted);
  font-size: 1.4rem;
  line-height: 1;
  /* Full touch target, drawn small. The × is 14 px of ink and needs 44 px of
     tappable area around it — which the height said 36 for, disagreeing with
     the comment right above it. */
  min-width: var(--touch);
  min-height: 44px;
  cursor: pointer;
}

.hotspot-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  /* Momentum scrolling inside the sheet, so the map underneath does not pan
     when the list is flicked. */
  overscroll-behavior: contain;
}

.hotspot-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 8px;
  width: 100%;
  min-height: 44px;
  padding: 7px 6px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font-family: var(--font);
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
}

.hotspot-list li:last-child .hotspot-row {
  border-bottom: 0;
}

.hotspot-row:active {
  background: var(--surface-raised);
}

.hotspot-row__name {
  font-weight: 600;
}

.hotspot-row__where {
  color: var(--danger);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.hotspot-row__meta {
  grid-column: 1 / -1;
  color: var(--text-muted);
  font-size: 0.76rem;
}

.hotspot-panel__note {
  margin: 6px 0 0;
}

/* The record-a-find button sits where the sheet does. The sheet is a
   destination-picking mode, and the button is not what you want mid-decision -
   but it must come back the moment the sheet is closed, so this is a class on
   the body rather than anything the button owns. */
body.is-focused .fab {
  opacity: 0;
  pointer-events: none;
}

/* ------------------------------------------------------------- data panel
 *
 * Pulling and generating data for the current view. Staff only, and inside
 * the layer panel, which is already narrow - so everything here is a full
 * width stack rather than a grid. On a phone this section is scrolled to,
 * not glanced at, and the vertical rhythm matters more than density.
 */

.data-panel { padding: 0 4px; }

.data-panel__species {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 8px 8px;
  margin: 8px 0;
}

.data-panel__species legend { padding: 0 4px; }

.data-panel__species-row {
  display: flex;
  align-items: center;
  gap: 8px;
  /* A full touch target per row. Ticking a mushroom in the rain with one
     hand is the whole use case, and a bare checkbox is not big enough. */
  min-height: var(--touch);
  font-size: 0.85rem;
}

/* The row is what gets tapped, but the box is what gets aimed at, and the
   browser default is 13 px of it. Matched to the layer menu's, which was
   already drawn at 20. */
.data-panel__species-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

.data-panel__kind { margin: 10px 0; }
.data-panel__kind .field__help { margin: 4px 0 0; }
.data-panel__kind button[disabled] { opacity: 0.5; }

.data-panel__jobs { margin-top: 12px; }

.data-panel__job-list {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
}

.data-panel__job {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 8px;
  margin-bottom: 4px;
  border-left: 3px solid var(--border);
  background: var(--surface-raised);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 0.85rem;
}

/* The status is carried by a colour on the edge rather than by an icon: it
   has to be readable at a glance, at arm's length, with wet hands. */
.data-panel__job--running { border-left-color: var(--accent); }
.data-panel__job--succeeded { border-left-color: var(--accent-strong); }
.data-panel__job--failed { border-left-color: var(--danger); }
.data-panel__job--cancelled { border-left-color: var(--text-muted); }

.data-panel__job button { align-self: flex-start; margin-top: 4px; }

/*
 * Secondary, not small to the finger.
 *
 * These are read as subordinate — starting the walk recorder, stopping a
 * running job — and used in the same conditions as everything else: outdoors,
 * one-handed, often wet. A 32 px target missed often enough to matter, so the
 * subordination is carried by type size and colour and the height stays at
 * the 44 px floor. Keeping it clear of the primary action is what stops a
 * stray thumb, not making it harder to hit.
 */
.btn--small {
  min-height: 44px;
  padding: 0 12px;
  font-size: 0.8rem;
}

.data-panel__jobs-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.data-panel__jobs-head h3 { margin: 0; }

/* The one button, and the parts it is made of.
 *
 * The primary action gets the accent and the space; the five parts sit inside
 * a closed disclosure, because needing one of them on its own is the
 * exception and five buttons in a required order is not a menu. */

.data-panel__kind--primary {
  margin: 4px 0 12px;
  padding: 8px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

.data-panel__kind--primary .data-panel__note {
  color: var(--text);
  font-weight: 600;
}

.data-panel__parts { margin-bottom: 8px; }

.data-panel__parts summary {
  cursor: pointer;
  /* A full touch target: this is opened with a thumb, in the rain. */
  min-height: var(--touch);
  display: flex;
  align-items: center;
}

/* --------------------------------------------------------------- recording a walk */

/* Above the primary action, centred on it. Far enough from the find button
   that a thumb reaching for one does not catch the other, and out of the
   bottom-right corner where the phone's own gestures live. */
.track-control {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  /* Same half-the-screen trap as `.fab` above. */
  width: max-content;
  max-width: calc(100% - 24px);
  /* Measured from the find button, which no longer carries a safe-area term
     of its own. See `.fab`. */
  bottom: calc(92px + var(--attribution-h));
  z-index: 5;

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.track-control .btn {
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  border: 1px solid var(--border);
}

/* A recording walk is the one state the map has that continues while the
   phone is in a pocket. It says so plainly rather than with a subtle tint,
   because the cost of not noticing is a track that stops at the car park. */
.track-control .btn[data-recording="true"] {
  background: var(--accent);
  color: #0d1408;
  border-color: transparent;
  font-weight: 700;
}

.track-control__status {
  padding: 3px 10px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--surface) 90%, transparent);
  border: 1px solid var(--border);
  font-size: 0.8rem;
  color: var(--text-muted);
  white-space: nowrap;
}

/* ------------------------------------------------------------ the season */

/* Three numbers across the top of the walk list. Kilometres walked is the
   denominator the finds are read against: three finds says one thing after
   two kilometres and another after thirty. */
.season-summary {
  display: flex;
  gap: var(--gap);
  text-align: center;
}

.season-summary__item {
  flex: 1 1 0;
  min-width: 0;
}

.season-summary__value {
  display: block;
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1.2;
}

.season-summary__label {
  display: block;
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* ==========================================================================
 * Breakpoints
 *
 * Last in the file on purpose. These override rules defined above them and
 * carry no more specificity than what they override, so source order is the
 * only thing that decides — put earlier, the phone layout lost to the
 * desktop one it was meant to replace, silently and only at phone widths.
 * ========================================================================== */

/*
 * Wide enough for a pointer: the destinations go back to the top bar.
 *
 * A bottom bar on a desktop window is a phone affordance in the wrong place —
 * the pointer is already at the top, and the bar would sit a screen's height
 * away from everything it relates to.
 */
@media (min-width: 700px) {
  .bottomnav { display: none; }
}

@media (max-width: 699.98px) {
  .topbar__nav { display: none; }

  /*
   * On the map, drop the bar entirely.
   *
   * What it has left there is the word "Map", which is precisely what the
   * highlighted destination below already says. Forty-eight pixels at the top
   * of a phone is a strip of forest, and the map is the one page where the
   * pixels are the content. Every other page keeps its title: «Walk
   * 18.08.2026» is not derivable from a highlighted tab.
   *
   * Browsers without :has() keep the bar, which is the old behaviour and
   * costs nothing but height.
   */
  body:has(main.main--map) .topbar { display: none; }

  /*
   * The layer panel becomes a sheet.
   *
   * It was a tray hanging under a button in the top corner: a web menu, at
   * the end of the screen a thumb reaches last, and drawn as a box that
   * appeared rather than as something raised. On a phone it now comes up from
   * the bar it was opened from, across the full width, with a grip at the top
   * and the map dimmed behind it.
   */
  .layer-toggle { display: none; }

  .layer-panel {
    position: fixed;
    left: 8px;
    right: 8px;
    top: auto;
    /* Clear of the bar, which is a flex item rather than a fixed one — so its
       height cannot be measured from here and is named instead. */
    bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom) + 8px);
    width: auto;
    max-height: 68vh;
    /* Above the bar, so the sheet and the button that raised it are not
       fighting over the same corner of the screen. */
    z-index: 40;
    border: 0;
    border-radius: 22px;
    padding: 0 8px 10px;
    box-shadow: 0 -8px 40px rgb(0 0 0 / 45%);
    overscroll-behavior: contain;
  }

  .layer-panel__grip {
    display: block;
    width: 2.25rem;
    height: 0.25rem;
    margin: 10px auto 6px;
    border-radius: 999px;
    background: var(--border);
  }
}
