/* geovibes — an art installation, not a form.
 *
 * Palette rule: the eight mood colours (src/lib/mood.ts) are the ONLY
 * saturated colour anywhere on this page. Everything else here is ink,
 * paper, or a translucent mix of the two.
 */

:root {
  --ink: #1b1a17;
  --paper: #faf8f4;
  --chrome-bg: rgba(250, 248, 244, 0.72);
  --chrome-border: rgba(27, 26, 23, 0.1);
  --chrome-shadow: rgba(27, 26, 23, 0.12);
  --muted: rgba(27, 26, 23, 0.6);
  --time-track: rgba(27, 26, 23, 0.22);
  --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --serif: ui-serif, Georgia, "Iowan Old Style", "Times New Roman", serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #f2efe9;
    --paper: #171613;
    --chrome-bg: rgba(23, 22, 19, 0.72);
    --chrome-border: rgba(242, 239, 233, 0.14);
    --chrome-shadow: rgba(0, 0, 0, 0.4);
    --muted: rgba(242, 239, 233, 0.62);
    --time-track: rgba(242, 239, 233, 0.28);
  }
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overscroll-behavior: none; /* the map owns the gestures; no pull-to-refresh fighting it */
  overflow-x: hidden;
}

body {
  font: 16px/1.5 var(--sans);
  color: var(--ink);
  background: var(--paper);
  -webkit-tap-highlight-color: transparent;
}

button, input, select, a, .legend-row, .compose-pill {
  touch-action: manipulation; /* kills the 300ms tap delay and double-tap zoom */
}

/* ---------- map: full-bleed, everything else floats over it ---------- */

#map {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100vh;  /* fallback for browsers without dvh */
  height: 100dvh; /* handles iOS Safari's collapsing toolbar */
}

.maplibregl-popup-content {
  font-family: var(--serif);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
}

.gv-popup-poem {
  margin: 0;
}

/* Both popup controls below (copy link, report) sit in one quiet row,
   right-aligned under the poem — this is an art piece, not a share bar or a
   moderation console, so neither one gets a border, a fill, or a count. */
.gv-popup-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: .1rem;
  margin-top: .5rem;
}

/* Small type and muted colour keep both from competing with the poem; the
   44px floor still applies so each is a real tap target on a phone, just a
   visually light one (generous padding, no border/fill until you're
   actually pointing at it). */
.gv-report-btn,
.gv-copy-link-btn {
  display: block;
  padding: .4rem .3rem;
  min-height: 44px;
  min-width: 44px;
  border: none;
  background: none;
  font-family: var(--sans);
  font-size: .72rem;
  color: var(--muted);
  cursor: pointer;
}

.gv-report-btn:hover,
.gv-report-btn:focus-visible,
.gv-copy-link-btn:hover,
.gv-copy-link-btn:focus-visible {
  color: var(--ink);
  text-decoration: underline;
}

.gv-report-btn:disabled,
.gv-copy-link-btn:disabled {
  cursor: default;
  opacity: .7;
}

/* ---------- header: a slim translucent bar, not a block ---------- */

header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: .5rem;
  padding: calc(env(safe-area-inset-top, 0px) + .6rem) calc(env(safe-area-inset-right, 0px) + 1rem) .6rem
    calc(env(safe-area-inset-left, 0px) + 1rem);
  background: var(--chrome-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--chrome-border);
}

h1 {
  font-family: var(--sans);
  font-size: 1rem;
  font-weight: 400;
  letter-spacing: .2em;
  text-transform: lowercase;
  margin: 0;
  color: var(--ink);
}

#auth {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: .85rem;
  color: var(--muted);
}

#auth .who {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- shared chrome controls ---------- */

button {
  font: inherit;
  font-family: var(--sans);
  min-height: 44px;
  min-width: 44px;
  padding: .5rem 1rem;
  border: 1px solid var(--chrome-border);
  background: var(--chrome-bg);
  color: var(--ink);
  border-radius: 999px;
  cursor: pointer;
}

button:hover { background: var(--chrome-border); }

button.ghost-btn {
  min-width: auto;
  padding: .4rem .8rem;
  font-size: .85rem;
}

button.primary-btn {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}

input, textarea, select {
  font: inherit;
  font-family: var(--sans);
  font-size: 16px; /* below 16px, iOS Safari zooms the page on focus and never zooms back */
  padding: .6rem .75rem;
  border: 1px solid var(--chrome-border);
  border-radius: 8px;
  background: var(--paper);
  color: var(--ink);
  min-height: 44px;
}

textarea {
  width: 100%;
  min-height: 6rem;
  font-family: var(--serif); /* poets read their own words in the face they'll be read in */
  resize: vertical;
}

/* ---------- legend: reachable, not shouting ---------- */

/* #panels anchors BOTH the mood key and the "lately" pill at the same fixed
   spot, side by side — one shared position, not two independently-anchored
   floating things that could drift apart or overlap. Anchored off the
   sheet's own measured height (--gv-sheet-h, kept live by main.ts) rather
   than a hard-coded offset — that height already includes the sheet's
   safe-area padding, and it shrinks when the sheet collapses, so the row
   follows the sheet up or down and is never overlapped by it in either
   state. The 5.5rem fallback only covers the instant before main.ts's first
   measurement lands.

   row-reverse + flex-end: the two pills sit bottom-aligned side by side: when
   one opens into a tall panel, it grows upward in place while the other's
   pill stays put at the same baseline — this is why #legend/#lately below no
   longer carry their own fixed position, just their own column layout. */
#panels {
  position: fixed;
  right: calc(env(safe-area-inset-right, 0px) + .75rem);
  bottom: calc(var(--gv-sheet-h, 5.5rem) + .6rem);
  z-index: 15;
  display: flex;
  flex-direction: row-reverse;
  align-items: flex-end;
  gap: .4rem;
  max-width: calc(100vw - 2rem - env(safe-area-inset-right, 0px));
}

#legend, #lately {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .4rem;
  min-width: 0;
}

/* Shared by both floating pills (#legend and #lately use the identical class
   names below — same collapsible pattern, on purpose, not a coincidence) */
.legend-toggle {
  font-size: .8rem;
  padding: .5rem .9rem;
  box-shadow: 0 2px 10px var(--chrome-shadow);
}

.legend-panel {
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--chrome-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--chrome-border);
  border-radius: 12px;
  padding: .4rem;
  box-shadow: 0 4px 18px var(--chrome-shadow);
  max-height: 60vh;
  overflow-y: auto;
  overflow-x: hidden;
  /* Constrained so it can never run off the right edge of a narrow phone —
     `.hidden` alone is not enough to keep it collapsed by default, see below. */
  width: max-content;
  max-width: calc(100vw - 2rem - env(safe-area-inset-right, 0px));
  box-sizing: border-box;
}

/* The `hidden` attribute normally beats nothing, but an *author* stylesheet
   rule always outranks the UA's own `[hidden] { display: none }`, even at
   equal specificity — so the unconditional `display: flex` above was quietly
   winning and the panel rendered open regardless of the `hidden` property.
   This makes the collapsed state actually collapse. */
.legend-panel[hidden] { display: none; }

.legend-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .2rem .4rem .3rem;
}

.legend-title {
  font-size: .85rem;
  font-weight: 600;
  color: var(--ink);
}

.legend-close {
  min-width: 36px;
  min-height: 36px;
  padding: 0;
  line-height: 1;
  font-size: 1.1rem;
}

.legend-note {
  margin: .3rem .4rem 0;
  font-size: .75rem;
  color: var(--muted);
  max-width: 100%;
  overflow-wrap: break-word;
}

.legend-row {
  display: flex;
  align-items: center;
  gap: .55rem;
  min-height: 44px;
  padding: .3rem .6rem;
  border: none;
  border-radius: 8px;
  background: transparent;
  font-size: .85rem;
  color: var(--ink);
  justify-content: flex-start;
  text-align: left;
  max-width: 100%;
  overflow-wrap: break-word;
}

.legend-row:hover { background: var(--chrome-border); }
.legend-row.active { background: var(--chrome-border); font-weight: 600; }

.legend-swatch {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex: none;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .5);
}

/* ---------- lately panel: the "lately" and "where there's writing" lists ----------
   .legend-panel/.legend-header/.legend-title/.legend-close/.legend-row/.legend-note
   are reused unchanged above, on purpose — same collapsible pattern as the
   mood key, not a rebuild of it. Only what's actually different about this
   panel's content gets new rules below. */

.lately-section-title {
  margin: .5rem .4rem .1rem;
  font-size: .75rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: lowercase;
  letter-spacing: .02em;
}

.lately-section-title:first-of-type { margin-top: .2rem; }

/* A poem row is taller than a mood-key row (two lines of real content) but
   keeps the same 44px floor and left-aligned, full-width tap target. */
.lately-row {
  flex-direction: column;
  align-items: stretch;
  gap: .15rem;
  min-height: 44px;
  padding: .45rem .6rem;
}

.lately-row-top {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.lately-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .5);
}

/* Poem text reads in the same serif the poems themselves are set in
   everywhere else on the page (compose textarea, map popups) — a UI label
   this is not. */
.lately-excerpt {
  font-family: var(--serif);
  font-size: .92rem;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}

.lately-meta {
  margin-left: 1.3rem; /* aligns under the excerpt, past the dot */
  font-size: .75rem;
  color: var(--muted);
}

.lately-place-row {
  justify-content: space-between;
}

.lately-count {
  color: var(--muted);
  font-size: .8rem;
}

.lately-empty {
  margin: .3rem .4rem;
  font-size: .8rem;
  color: var(--muted);
}

/* ---------- sheet: collapsible wrapper around the compose section ---------- */

/* #sheet owns the fixed position, chrome (blur/border/shadow) and the
   collapse animation; #compose (below) keeps its own content padding.
   Splitting them means collapsing the sheet doesn't require main.ts or
   compose.ts to know anything about it — they just keep rendering into
   #compose as before, and #sheet clips around whatever's there.

   Collapse is driven by --gv-sheet-h, a pixel height main.ts keeps live via
   ResizeObserver: the natural content height while expanded, or just the
   handle's height while collapsed. max-height transitions between the two,
   and overflow: hidden means the collapsed box is ACTUALLY that short — not
   just visually clipped — so the map underneath gets real touch events back,
   with no leftover dead zone where the sheet used to be. */
#sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 25;
  display: flex;
  flex-direction: column;
  background: var(--chrome-bg);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border-top: 1px solid var(--chrome-border);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -4px 20px var(--chrome-shadow);
  /* Safe-area bottom inset lives here, once, so it's reserved whether the
     sheet is showing full content or just the handle. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
  overflow: hidden;
  max-height: var(--gv-sheet-h, 80vh);
  transition: max-height .3s ease;
}

/* Pre-JS default for a returning visitor who left the sheet collapsed (see
   the inline script in index.html's <head>) — avoids a flash of the full
   sheet before main.ts's first measurement lands and takes over. 44px
   matches #sheet-handle's own min-height below. */
html.gv-sheet-collapsed #sheet {
  max-height: calc(44px + env(safe-area-inset-bottom, 0px));
}
html.gv-sheet-collapsed .sheet-chevron {
  transform: rotate(180deg);
}

/* The handle is the only thing guaranteed visible once collapsed, so it is
   never allowed to shrink below the 44px tap-target floor, and it never
   scrolls or clips with the content below it (flex: none). */
#sheet-handle {
  flex: none;
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0;
  margin: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--ink);
}

#sheet-handle:hover { background: var(--chrome-border); }

.sheet-chevron {
  transition: transform .25s ease;
}

/* Down when expanded (tap to hide), up when collapsed (tap to show) —
   toggling one class handles both the box's height and the chevron's
   direction, so they can never drift out of sync with each other. */
#sheet.collapsed .sheet-chevron {
  transform: rotate(180deg);
}

#compose {
  padding: .75rem calc(env(safe-area-inset-right, 0px) + 1rem) .9rem
    calc(env(safe-area-inset-left, 0px) + 1rem);
  min-height: 0;
}

.compose-pill {
  display: block;
  width: 100%;
  text-align: center;
  font-size: .95rem;
  padding: .8rem 1rem;
}

.compose-panel {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  position: relative;
}

.compose-close {
  position: absolute;
  top: -.25rem;
  right: -.25rem;
  min-width: 36px;
  min-height: 36px;
  padding: 0;
  line-height: 1;
  font-size: 1.1rem;
}

.compose-row {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: center;
}

.compose-row label {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 44px;
  font-size: .9rem;
  color: var(--ink);
}

.compose-row input[type="checkbox"],
.compose-row input[type="radio"] {
  min-height: auto;
  width: 20px;
  height: 20px;
  padding: 0;
}

.compose-actions {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
}

.count { font-size: .85rem; color: var(--muted); }
.count.over { color: #c0392b; font-weight: 600; } /* error red — not a mood, exempt from the palette rule */

.sheet-hint { margin: 0 0 .25rem; font-size: .85rem; color: var(--muted); }

.signin-sheet {
  display: flex;
  flex-direction: column;
  gap: .5rem;
}
.signin-sheet .compose-row { align-items: stretch; }
.signin-sheet input { flex: 1; min-width: 0; }

/* ---------- toasts: unobtrusive, floating, but load-bearing ---------- */

#toasts {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 3.4rem);
  left: calc(env(safe-area-inset-left, 0px) + .75rem);
  right: calc(env(safe-area-inset-right, 0px) + .75rem);
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: .4rem;
  pointer-events: none;
}

#toasts p {
  margin: 0;
  padding: .6rem .9rem;
  border-radius: 10px;
  font-size: .85rem;
  box-shadow: 0 2px 12px var(--chrome-shadow);
  pointer-events: auto;
}

#relay-warning {
  background: var(--chrome-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--chrome-border);
  color: var(--ink);
}

#error {
  background: rgba(192, 57, 43, .14);
  border: 1px solid rgba(192, 57, 43, .3);
  color: #c0392b;
}

/* ---------- crosshair: aiming a remote placement at the map centre ---------- */

.gv-crosshair {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34px;
  height: 34px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 5;
}

.gv-crosshair::before,
.gv-crosshair::after {
  content: "";
  position: absolute;
  background: var(--ink);
  opacity: .55;
}

.gv-crosshair::before {
  top: 50%;
  left: 0;
  right: 0;
  height: 1px;
  transform: translateY(-50%);
}

.gv-crosshair::after {
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  transform: translateX(-50%);
}

.gv-crosshair-ring {
  position: absolute;
  inset: 25%;
  border: 1.5px solid var(--ink);
  border-radius: 50%;
  opacity: .55;
}

/* ---------- drift caption ----------
   Lives in the lower third, anchored off the sheet's own measured height
   (--gv-sheet-h, kept live by main.ts) the same way #panels above anchors
   the mood key and "lately" pill — never a hard-coded offset, so the
   caption follows the sheet up or down and is never overlapped by it, the
   wordmark, or the relay-warning toast, which all live elsewhere (top /
   bottom-right). Centred and width-constrained so a long poem never
   overflows the viewport at phone widths. Fades, never appears/disappears
   sharply — this is a caption on a quietly moving screen, not a toast.
   pointer-events: none so it never intercepts the tap that's meant to stop
   drift; the global listener in drift.ts hears that tap regardless of what
   DOM element it lands on. */
#drift-caption {
  position: fixed;
  left: 50%;
  bottom: calc(var(--gv-sheet-h, 5.5rem) + 1.4rem);
  z-index: 12;
  max-width: min(34rem, calc(100vw - 2.5rem));
  transform: translateX(-50%);
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1s ease;
  padding: .9rem 1.2rem;
  border-radius: 14px;
  background: var(--chrome-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 4px 18px var(--chrome-shadow);
}

#drift-caption.visible { opacity: 1; }

#drift-place {
  margin: 0 0 .35rem;
  font-family: var(--sans);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The same serif every poem is set in elsewhere on the page (compose
   textarea, map popups, "lately" excerpts) — a caption reading it, not a UI
   label describing it. */
#drift-text {
  margin: 0;
  font-family: var(--serif);
  font-size: 1.1rem;
  line-height: 1.55;
  color: var(--ink);
}

/* The one quiet affordance: enough to know the map is driving itself and
   that touching it hands control back — no button, because there is
   nothing to press. */
#drift-hint {
  margin: .6rem 0 0;
  font-family: var(--sans);
  font-size: .68rem;
  color: var(--muted);
}

/* No reduced-motion override needed here: the file's own blanket rule
   (bottom of this file, `* { transition: none !important }`) already covers
   this fade, and drift never starts in the first place under that setting
   (see drift.ts). */

/* ---------- time: the map's own history ----------
   Stacked ABOVE the #panels row (mood key / "lately") rather than beside it,
   both anchored off the same measured sheet height, so on a phone the track
   keeps its full width instead of being squeezed against two pills. It shares
   the lower third with the drift caption, which is safe: any touch stops
   drift, and reaching this control is a touch.

   Ink and paper only — no saturated colour — because the eight mood colours
   are the map's alphabet, and a scrubber borrowing one would read as a
   ninth mood. */
#time {
  position: fixed;
  left: 50%;
  bottom: calc(var(--gv-sheet-h, 5.5rem) + 3.3rem);
  z-index: 14;
  transform: translateX(-50%);
  width: min(30rem, calc(100vw - 2.5rem));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .35rem;
  padding: .55rem .9rem .35rem;
  border-radius: 14px;
  background: var(--chrome-bg);
  border: 1px solid var(--chrome-border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 4px 18px var(--chrome-shadow);
  /* Quiet at rest, but never invisible. The first version faded to .5, which
     over the pale CARTO basemap left the word "now" floating in open country
     with no panel and no track under it — a control nobody could find is not
     restraint, it is a bug. .82 is as far down as it goes while the track
     still reads. */
  opacity: .82;
  transition: opacity .4s ease;
}

#time:hover, #time:focus-within, #time.scrubbed { opacity: 1; }

/* Reads "now" at rest, "yesterday — back to now" once moved. It is only a
   button when there is somewhere to go back to; disabled at the live edge so
   nothing invites a press that would do nothing. */
#time-label {
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  font-family: var(--sans);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: lowercase;
  color: var(--muted);
  cursor: pointer;
}

#time-label:disabled { cursor: default; }
#time.scrubbed #time-label { color: var(--ink); }

#time-slider {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 1.4rem;
  background: none;
  cursor: ew-resize;
}

/* The track is its own tone, not --chrome-border: that border is 10% ink,
   which is right for a hairline between two panels and far too faint to be
   the one line the reader has to aim a thumb at. */
#time-slider::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 3px;
  background: var(--time-track);
}
#time-slider::-moz-range-track {
  height: 3px;
  border-radius: 3px;
  background: var(--time-track);
}

/* A small solid dot, the same shape vocabulary as a pin — the reader is
   dragging a point along a line, which is exactly what the map is doing. */
#time-slider::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--paper);
  box-shadow: 0 1px 4px var(--chrome-shadow);
}
#time-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 2px solid var(--paper);
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 1px 4px var(--chrome-shadow);
}

#time-slider:focus-visible { outline: none; }
#time-slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--chrome-border); }
#time-slider:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px var(--chrome-border); }

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

.gv-pin { pointer-events: auto; }

@media (prefers-reduced-motion: no-preference) {
  .gv-pin, .legend-row, .compose-pill, .compose-panel, button {
    transition: opacity .25s ease, background-color .2s ease;
  }

  /* A poem whose time is up, going. Longer and slower than the ordinary pin
     transition above so the disappearance is something you watch rather than
     something you catch — this is the moment the whole "chalk in the rain"
     idea is about, and it is over in a quarter second otherwise.

     ease-in, not ease-out: it holds its colour for a beat and then goes, which
     is how something fades away rather than how a UI element dismisses.

     Opacity only — no transform. The .gv-pin element IS the MapLibre marker,
     and MapLibre rewrites its transform every frame to hold it over its
     coordinate, so any scale here would be fighting the map for one property.

     Under `prefers-reduced-motion: reduce` the blanket rule below kills this
     transition and the poem simply stops being drawn. That is the honest
     outcome for that setting: the fade is the part that was asked to stop. */
  .gv-pin.gv-departing {
    transition: opacity 1.4s ease-in;
  }
}

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