/* Hiking app prototype — design system per
   ../designer/outputs/2026-08-16-hiking-app-visual-concept.md
   Light only: dark mode is explicitly out of scope this pass (§7.1). */

:root {
  /* §1.1 palette */
  --paper:  #F7F4EC;   /* warm near-white base surface */
  --ink:    #1C2620;   /* deep forest-charcoal: text + map chrome */
  --green:  #2F5233;   /* trail green: nav, primary buttons, trail lines */
  --accent: #E8724C;   /* sunrise orange — pin motif ONLY, see rule below */
  --tan:    #C9B896;   /* trail-dust tan: cards, dividers */
  --slate:  #5C6B73;   /* slate blue-gray: secondary text + icons */
  --danger: #C0392B;   /* system-convention destructive red: Sign Out only */
  /* Muted error, for inline validation that should not shout the way a
     destructive action does. Deliberately lower-chroma than --danger, and it
     measures better against paper too: 5.6:1 vs the destructive red's 4.9:1.

     This is also the login concept's inline field-error color (§1.8/§3), in
     place of the new #A8442E that concept proposed. DA's review was right
     that #A8442E is not hue-distinct from the reserved --accent (3.8 apart,
     not the claimed distinct hue); it took the same objection to this token,
     which sits 5.7 away. The measured answer — see
     .build-tmp/color-check.js — is that hue is the wrong axis to settle it
     on: against --accent this token is dE00 21.3 normal, 18.8 protanopia,
     21.2 deuteranopia, i.e. clearly separate for dichromats too, and it beats
     #A8442E on every one of those (19.3 / 18.1 / 18.7). Adding a sixth red to
     score marginally worse than a token already carrying exactly this job
     would be the wrong trade. */
  --danger-muted: #9A4A3C;

  /* the one glyph the inline error needs; masked so it takes the text color */
  --icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a7 7 0 100 14A7 7 0 008 1zm0 3.2a.9.9 0 01.9.95l-.25 3.6a.65.65 0 01-1.3 0l-.25-3.6A.9.9 0 018 4.2zm0 6.5a.95.95 0 110 1.9.95.95 0 010-1.9z'/%3E%3C/svg%3E");

  /* ACCENT RULE (§1.1, narrowed): --accent may only be used by
     (a) the photo pin / pinned state of a post,
     (b) the live-recording ring,
     (c) the offline-download progress ring.
     Anything else uses green / ink / slate / tan. */

  --paper-raised: #FFFDF7;
  --hairline: rgba(28, 38, 32, .12);

  --r-sm: 8px;
  --r-md: 14px;
  --r-lg: 22px;

  /* §1.2 type: rounded geometric sans for headings, system sans for UI,
     tabular figures for measured trail data. All system faces — no webfont
     fetch, so the prototype works offline like the app it describes. */
  --font-head: ui-rounded, "SF Pro Rounded", "Nunito Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-ui: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif;

  --ease: cubic-bezier(.22, 1, .36, 1);
  --dur: 240ms;
}

* { box-sizing: border-box; }

/* Components here set `display` on their own class, which ties with and then
   outranks the UA's `[hidden]` rule — so `el.hidden = true` silently does
   nothing. One global guard instead of a per-component one each time. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  font-family: var(--font-ui);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

body { background: #E7E2D6; }

.stage {
  width: 100%;
  min-height: 100%;
  display: grid;
  place-items: center;
}

button { font: inherit; color: inherit; cursor: pointer; }

.stat-num,
.stats dd,
.region-est strong {
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}

/* ---------- device frame ---------- */

.device {
  width: 390px;
  height: 844px;
  background: var(--paper);
  border-radius: 44px;
  box-shadow: 0 30px 70px rgba(28, 38, 32, .28), 0 0 0 10px #20282A;
  overflow: hidden;
  position: relative;
}

.app {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.screens { position: relative; flex: 1; min-height: 0; }

.screen {
  position: absolute;
  inset: 0;
  display: none;
  background: var(--paper);
}
.screen.is-active { display: block; }
.screen--scroll { overflow-y: auto; -webkit-overflow-scrolling: touch; padding-bottom: 16px; }

.screen-head {
  padding: 58px 20px 0;
}
.screen-head h1 {
  font-family: var(--font-head);
  font-size: 28px;
  line-height: 1.15;
  margin: 0 0 8px;
  letter-spacing: -.01em;
}

/* §1.3: the "signature squiggle" — one thin contour line under section
   headers. Not a pattern fill, not a background. */
.contour-rule {
  height: 7px;
  background: no-repeat left center / 128px 7px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='7' viewBox='0 0 128 7'%3E%3Cpath d='M0 5C12 5 14 2 26 2s14 3 26 3 14-3 26-3 14 3 26 3 14-2 24-2' fill='none' stroke='%23C9B896' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  margin-bottom: 14px;
}

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

.map {
  position: absolute;
  inset: 0;
  background: #EDE7D9;
  overflow: hidden;
  touch-action: none;
}
/* Real Mapbox tiles sit behind the SVG overlay; the beige plate above is
   what shows when there's no token and the synthetic map draws instead.

   The `.map >` is load-bearing, not tidiness. GL JS adds `.mapboxgl-map` to
   whatever element it's given as a container, and mapbox-gl.css — injected
   into <head> at runtime, so always *after* this file — sets
   `.mapboxgl-map { position: relative }`. Equal specificity, later wins:
   a bare `.map-basemap` rule loses, `inset: 0` stops applying and the
   container collapses to zero height. Descendant selector = (0,2,0), which
   outranks it. */
.map > .map-basemap { position: absolute; inset: 0; }

/* Mapbox's required attribution and logo default to the bottom corners of
   the map container — which is exactly where this app's bottom sheets live:
   the record sheet (113px collapsed, 139px expanded), the trail sheet
   (197px) and the offline region bar. Measured: the attribution box sits
   723–747 and the collapsed record sheet 644–757, so it is 100% covered.
   That's a Mapbox ToS problem as much as a visual one — attribution has to
   be reasonably visible, not merely present in the DOM.

   Both controls move to the top of the map, stacked rather than side by
   side: the expanded attribution is 335px wide on a 390px frame, so it
   cannot share a row with the logo. The clear band differs per screen —
   the map screen's title bar ends at 80, the pin screen's topbar + photo
   thumb at 188 — so the two screens carrying a basemap get their own
   offsets. Measured, not guessed; see the build note.

   This is a mechanical fix for an overlap, NOT a design decision. If
   Designer wants them somewhere else, only these four offsets change. */
[data-screen="map"] .map > .map-basemap .mapboxgl-ctrl-bottom-left { top: 84px; bottom: auto; }
[data-screen="map"] .map > .map-basemap .mapboxgl-ctrl-bottom-right { top: 112px; bottom: auto; }
[data-screen="post"] .map > .map-basemap .mapboxgl-ctrl-bottom-left { top: 196px; bottom: auto; }
[data-screen="post"] .map > .map-basemap .mapboxgl-ctrl-bottom-right { top: 224px; bottom: auto; }
.map.is-basemap { background: #1C2620; }
.map-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.map-contours path { fill: none; stroke: var(--slate); stroke-opacity: .22; stroke-width: 1; }
.map-trails path { fill: none; stroke: var(--green); stroke-width: 3.5; stroke-linecap: round; stroke-linejoin: round; }
.map-trails path.is-recording { stroke-dasharray: none; }
/* fat invisible hit line: `pointer-events: stroke` is what makes a
   transparent stroke tappable at all */
.map-trails path.hit { stroke: transparent; stroke-width: 22; pointer-events: stroke; cursor: pointer; }

.map-pins { position: absolute; inset: 0; pointer-events: none; }
.map-pins > * { pointer-events: auto; }

.map-topbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding: 52px 18px 10px;
  background: linear-gradient(180deg, rgba(28,38,32,.55), rgba(28,38,32,0));
  pointer-events: none;
}
.map-title {
  font-family: var(--font-head);
  font-size: 15px;
  font-weight: 600;
  color: #fff;
  text-shadow: 0 1px 3px rgba(28,38,32,.6);
}

/* ---------- recentre control ----------
   Floating bottom-right of the map, 16px in from the right edge and 16px
   above the top edge of whichever bottom sheet is open — the record sheet's
   *collapsed* edge by default, the trail sheet's when that's up instead.
   --recentre-lift is that height, measured from the sheets themselves rather
   than hardcoded (measureRecentreLift() in app.js, re-run on every sheet
   toggle and on resize), and deliberately not re-measured while the record
   sheet is expanded: the spec anchors this to the collapsed edge, so the
   control holds still while that sheet moves under it. The fallback matches
   the collapsed record sheet at the 390x844 reference frame, for the paint
   before the first measurement lands.

   Hidden state is visibility, not `hidden`/`display` — opacity needs a box
   to transition, and visibility still takes it out of the tab order and the
   a11y tree. The global prefers-reduced-motion rule collapses the fade. */
.recentre {
  position: absolute;
  right: 16px;
  bottom: calc(var(--recentre-lift, 104px) + 16px);
  width: 44px; height: 44px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 2px solid var(--green);
  border-radius: 50%;
  background: var(--paper);
  color: var(--green);
  /* the sheet's elevation, re-pointed upward for a floating control rather
     than a surface rising off the bottom edge */
  box-shadow: 0 8px 30px rgba(28, 38, 32, .18);
  opacity: 0;
  visibility: hidden;
  transition: opacity 170ms ease, visibility 0s linear 170ms;
}
.recentre.is-shown {
  opacity: 1;
  visibility: visible;
  transition: opacity 170ms ease;
}
.recentre:active { background: var(--paper-raised); }

/* ---------- hiking-trails basemap toggle ----------
   Top-right, below the topbar gradient, so it never shares a corner with the
   recentre control. Unlike that control this one is permanent chrome — it's
   a mode the user picks, not an affordance that appears when there's
   something to fix — so it carries a label rather than a bare glyph. */
.maplayer {
  position: absolute;
  top: 56px;
  right: 16px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px 7px 10px;
  border: 2px solid var(--green);
  border-radius: 999px;
  background: var(--paper);
  color: var(--green);
  font-family: var(--font-head);
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 8px 30px rgba(28, 38, 32, .18);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
/* pressed state is a fill, not a tint: at this size a border-only difference
   between on and off is not a state you can read at a glance outdoors */
.maplayer.is-on {
  background: var(--green);
  color: var(--paper);
}
.maplayer:active { transform: translateY(1px); }

/* ---------- place search ----------
   Its own row, below everything already in the top band. Measured, not
   guessed (.build-tmp/attrib-geometry.js): title 53-73, Trails pill 56-94,
   then the Mapbox logo at 84-107 and the attribution control at 112-156 —
   the two the basemap pass moved up here *because* the bottom sheets were
   covering them, which is a ToS obligation and not mine to undo. 156 is
   therefore the first genuinely clear line; 164 is that plus the 8px this
   stack is spaced on.

   Sitting under the attribution rather than over it also means the results
   panel, which drops from 214, never covers it either — so attribution stays
   visible in every state of this control, not just its resting one.

   Full width rather than tucked beside something — a place name plus its
   region is a long string, and truncating the thing you just typed is the
   fastest way to make a search box feel broken.

   Permanent chrome, so it takes the Trails toggle's treatment (paper fill,
   2px trail-green, the shared floating elevation) rather than the recentre
   control's fade-in-when-needed one. No orange anywhere: search is navigation
   utility, not a capture state (§1.1 accent rule). */
.placesearch {
  position: absolute;
  top: 164px;
  left: 16px;
  right: 16px;
}
.placesearch-field {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 42px;
  padding: 0 6px 0 13px;
  border: 2px solid var(--green);
  border-radius: 999px;
  background: var(--paper);
  color: var(--green);
  box-shadow: 0 8px 30px rgba(28, 38, 32, .18);
}
/* Focus lands on the input, but the *field* is what reads as the control, so
   the ring goes on the wrapper — via :focus-within, which is why the input
   itself drops its own outline rather than showing two rings inside each
   other. Not `outline: none` on the field: it keeps a real focus indicator. */
.placesearch-field:focus-within {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}
.placesearch-icon { flex: none; }
.placesearch-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  outline: 0;
  background: none;
  padding: 0;
  font-family: var(--font-ui);
  /* 16px exactly: iOS Safari zooms the whole page when a text field smaller
     than this takes focus, which on a map prototype would throw the camera
     and the chrome out of frame on the first tap. */
  font-size: 16px;
  color: var(--ink);
}
.placesearch-input::placeholder { color: var(--slate); opacity: 1; }
/* the UA's own clear affordance would sit next to ours */
.placesearch-input::-webkit-search-cancel-button { display: none; }
.placesearch-clear {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--slate);
  cursor: pointer;
}
.placesearch-clear:active { background: var(--hairline); }

/* The results surface is a surface, not a floating control, so it takes the
   sheet's paper-and-shadow elevation instead of a second 2px green outline
   stacked under the field's. */
.placesearch-panel {
  margin-top: 8px;
  /* Five two-line rows exactly (5x58 + 4 dividers = 294), which is the
     SEARCH_LIMIT the query asks for — so the common case shows a whole list
     rather than clipping the last row mid-text and reading like a bug. The
     overflow is still there for the uncommon one: a long name that wraps. */
  max-height: 296px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--r-md);
  background: var(--paper);
  box-shadow: 0 8px 30px rgba(28, 38, 32, .22);
}
.placesearch-list { margin: 0; padding: 0; list-style: none; }
.placesearch-opt {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* 48px+ tall in practice: two lines of text at this padding. Comfortably
     over the 44pt target the rest of the map chrome is built to. */
  padding: 10px 14px;
  cursor: pointer;
}
.placesearch-opt + .placesearch-opt { border-top: 1px solid var(--hairline); }
/* One highlight for both pointer hover and keyboard cursor, and it's a fill
   rather than a tint — the same call .maplayer.is-on makes, for the same
   measured reason. A 10% green wash over paper leaves the 12.5px region line
   at 4.25:1, under AA; the solid fill puts both lines at 8.0:1. Hierarchy
   between the two lines then comes from size and weight, which survives the
   fill, rather than from a second colour, which doesn't. */
.placesearch-opt:hover,
.placesearch-opt.is-active { background: var(--green); }
.placesearch-opt-name {
  font-family: var(--font-head);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.placesearch-opt-where { font-size: 12.5px; color: var(--slate); }
.placesearch-opt:hover .placesearch-opt-name,
.placesearch-opt:hover .placesearch-opt-where,
.placesearch-opt.is-active .placesearch-opt-name,
.placesearch-opt.is-active .placesearch-opt-where { color: var(--paper); }
.placesearch-msg {
  margin: 0;
  padding: 12px 14px;
  font-size: 13.5px;
  color: var(--slate);
}

/* ---------- no-token notice ----------
   Same row, same insets as .placesearch above — it stands in for the search
   bar rather than sitting beside it, so it inherits that row's geometry
   verbatim instead of picking new numbers (top: 164 is where the top band
   ends; the Mapbox logo and attribution above it are a ToS obligation).

   Neutral chrome, not an error: paper-raised fill, ink text, a hairline
   border — the materials the sheets and cards already use. Explicitly not
   --danger (reserved for destructive actions and real validation failures)
   and not --accent (the §1.1 rule scopes sunrise orange to the pin motif,
   the recording ring and the download ring). Nothing here is broken and
   nothing here is a capture state; a no-token build is a supported one.

   No fixed height and no truncation: the copy names all three things that
   disappear, which wraps to two lines at the 390px reference width. A
   legible two-line notice beats a clipped one-liner — the whole point of
   this element is that it gets read. */
.map-notice {
  position: absolute;
  top: 164px;
  left: 16px;
  right: 16px;
  padding: 10px 14px;
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  background: var(--paper-raised);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.4;
  text-align: center;
}

/* ---------- live GPS marker ----------
   The device's real fix. Deliberately NOT a flame: a third mark type that no
   pin state can be confused with. Dual-tone (ink fill + paper ring) for the
   same reason the friends-on-map badge is — no single hue clears both pale
   terrain and the green trail line, but the two together always do. */
.live-gps {
  position: absolute;
  transform: translate(-50%, -50%);   /* centre-anchored: the puck's centre IS
                                          the fix — unlike a pin's tail */
  pointer-events: none;
}
/* reported accuracy, to scale, centred on the fix. Centred with the standalone
   `translate` property, not `transform`, because the reused `pulse` keyframe
   animates `transform` — a translate() here would be overwritten by it and the
   halo would jump off-centre for the whole animation. */
.live-gps__halo {
  position: absolute;
  left: 50%; top: 50%;
  translate: -50% -50%;
  border-radius: 50%;
  background: rgba(93, 138, 106, .18);
  border: 1px solid rgba(93, 138, 106, .45);
  animation: pulse 1.9s var(--ease) infinite;   /* reuses .record-ring's keyframe */
}
/* 10px ink + 2px paper ring = 14px across: never competes with a 44–52px pin,
   and always smaller than the halo's 24px floor. Written as 14px because this
   sheet is globally border-box (line 57) — the spec's literal `width: 10px`
   would have given a 10px puck with a 6px ink core. */
.live-gps__puck {
  position: absolute;
  left: 50%; top: 50%;
  width: 14px; height: 14px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--paper);
  box-shadow: 0 1px 3px rgba(28, 38, 32, .35);
}

/* ---------- circular photo pin (§4/§6) ----------
   ONE component. Map, feed, profile grid and downloads all render this exact
   markup at a different --pin-size; nothing else is allowed to draw a pin. */

/* --pin-size is the frame's nominal width, and the head circle inside it is
   92% of that — both unchanged by the swap from a vector frame to the image
   cutout, so no pin anywhere changes size. What did change is that the frame
   is now flame-pin.png, whose own file box is exactly the artwork's bounding
   box (367x512, the master with its transparent pad cropped off). Every
   number below is that box measured, then scaled so the head lands at .92:

     .9225 / 1.2870   the image box            (367 and 512, x .92/366)
     .0410 / .0372    the offset that puts the image's hole centre on the
                      pin box's centre, where .pin-photo already sits
     .4155            the hole's diameter      (165.3, x .92/366)
     132.4%           image top + image height — the tail point, which is
                      what the map coordinate is, not the bottom of the head

   Measured in .build-tmp/radius-profile.js; app.js's frame comment carries
   the raw pixel figures. */
.photo-pin {
  --pin-size: 44px;
  width: var(--pin-size);
  height: var(--pin-size);
  position: relative;
  display: block;
  padding: 0;
  border: 0;
  background: none;
  flex: none;
}
.pin-frame {
  position: absolute;
  left: calc(var(--pin-size) * .0410);
  top: calc(var(--pin-size) * .0372);
  width: calc(var(--pin-size) * .9225);
  height: calc(var(--pin-size) * 1.2870);
  filter: drop-shadow(0 2px 6px rgba(28, 38, 32, .35));
}
/* the photo sits in the frame's hole — a circular crop, unchanged (A.3) */
.pin-photo {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--pin-size) * .4155);   /* the cutout's own hole, see above */
  height: calc(var(--pin-size) * .4155);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--tan) center/cover no-repeat;
}
.photo-pin--map {
  position: absolute;
  transform: translate(-50%, -132.4%);
  transition: transform var(--dur) var(--ease);
}
.photo-pin--map.is-focused { transform: translate(-50%, -132.4%) scale(1.18); }

/* ---------- the flame mark on its own (branding B.2, intro, drop pin) ----
   Same asset and same fractions as .pin-frame, except the element's box IS
   the image's box — so whatever positions a mark is positioning the artwork
   itself, with no padding to reason about. app.js sets --pin-size on it. */
.flame-mark {
  position: relative;
  display: block;
  width: calc(var(--pin-size) * .9225);
  height: calc(var(--pin-size) * 1.2870);
}
.flame-mark__img { display: block; width: 100%; height: 100%; }
/* The photo hole, filled with paper because nothing sits behind this one.
   Percentages here are of the image's own box, so they're the raw
   measurements: centre (182.6, 184.1) of 367x512, diameter 165.3 of 367. */
.flame-mark__hole {
  position: absolute;
  left: 49.76%;
  top: 35.96%;
  width: 45.04%;
  height: calc(var(--pin-size) * .9225 * .4504);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--paper);
}

/* ---------- sheets ---------- */

.sheet {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  background: var(--paper-raised);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -8px 30px rgba(28, 38, 32, .18);
  padding: 12px 20px 22px;
}
.sheet-grabber {
  display: block;
  width: 44px; height: 5px;
  margin: 0 auto 10px;
  border: 0; padding: 0;
  border-radius: 3px;
  background: var(--tan);
}
.sheet-close {
  position: absolute;
  top: 12px; right: 14px;
  border: 0; background: none;
  color: var(--slate);
  font-size: 15px;
  line-height: 1;
  padding: 6px;
}
.sheet-title { font-family: var(--font-head); font-size: 20px; margin: 4px 0 8px; }

/* collapsed: distance + the record button only. Expanded: the full live
   readout and the state line. */
.sheet--record:not(.is-expanded) .stat:nth-child(n+2) { display: none; }
.sheet--trail { padding-top: 18px; }
.sheet--pin { text-align: center; }

.stats { display: flex; gap: 22px; margin: 0; }
.stats--live { flex: 1; }
.stat dt { font-size: 11px; color: var(--slate); margin: 0 0 2px; }
.stat dd { margin: 0; font-size: 18px; font-weight: 600; }

.record-row { display: flex; align-items: center; gap: 16px; }

/* record button: trail-green when idle; accent (b) only as the live ring */
.record-btn {
  position: relative;
  width: 64px; height: 64px;
  border: 0; border-radius: 50%;
  background: none;
  flex: none;
}
.record-core {
  position: absolute; inset: 6px;
  border-radius: 50%;
  background: var(--green);
  transition: border-radius var(--dur) var(--ease), inset var(--dur) var(--ease);
}
.record-btn[aria-pressed="true"] .record-core { inset: 20px; border-radius: 5px; }
.record-ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 3px solid transparent;
}
.record-btn[aria-pressed="true"] .record-ring {
  border-color: var(--accent);
  animation: pulse 1.9s var(--ease) infinite;
}
@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.09); opacity: .55; }
}
.record-hint { margin: 10px 0 0; font-size: 12px; color: var(--slate); }

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

.btn {
  border: 0;
  border-radius: 999px;
  padding: 11px 18px;
  font-size: 15px;
  font-weight: 600;
  font-family: var(--font-head);
  background: var(--tan);
  color: var(--ink);
  transition: filter 160ms var(--ease);
}
.btn:hover { filter: brightness(.96); }
.btn--primary { background: var(--green); color: #fff; }
.btn--quiet { background: transparent; color: var(--slate); padding-inline: 10px; }
.btn--wide { width: 100%; }
.btn[disabled] { opacity: .5; cursor: default; }

/* ---------- offline download (§3) ---------- */

.region-picker { position: absolute; inset: 0; }
.region-scrim { position: absolute; inset: 0; background: rgba(28,38,32,.35); }
.region-box {
  position: absolute;
  border: 2px solid var(--paper);
  border-radius: var(--r-sm);
  box-shadow: 0 0 0 9999px rgba(28,38,32,.32);
  background: rgba(247,244,236,.08);
  touch-action: none;
  cursor: move;
}
.region-handle {
  position: absolute;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--green);
  touch-action: none;
}
.region-handle[data-corner="nw"] { top: -11px; left: -11px; cursor: nwse-resize; }
.region-handle[data-corner="ne"] { top: -11px; right: -11px; cursor: nesw-resize; }
.region-handle[data-corner="sw"] { bottom: -11px; left: -11px; cursor: nesw-resize; }
.region-handle[data-corner="se"] { bottom: -11px; right: -11px; cursor: nwse-resize; }

.region-bar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  background: var(--paper-raised);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: 16px 20px 22px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.region-est-label { display: block; font-size: 11px; color: var(--slate); }
.region-est strong { font-size: 18px; }
.region-actions { display: flex; align-items: center; gap: 6px; }

.download-list { padding: 0 20px; display: flex; flex-direction: column; gap: 10px; }
.dl-row {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--paper-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  padding: 14px;
}
.dl-body { flex: 1; min-width: 0; }
.dl-name { font-family: var(--font-head); font-weight: 600; font-size: 16px; }
.dl-meta { font-size: 12px; color: var(--slate); margin-top: 3px; }

/* progress ring — accent (c). Shape differs from the ready badge below, so
   "downloading" vs "ready offline" reads without color or text (§3). */
.dl-ring {
  --pct: 0;
  width: 40px; height: 40px;
  border-radius: 50%;
  flex: none;
  background:
    conic-gradient(var(--accent) calc(var(--pct) * 1%), rgba(201,184,150,.5) 0);
  display: grid;
  place-items: center;
}
.dl-ring::after {
  content: "";
  width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--paper-raised);
}
.dl-badge {
  width: 40px; height: 40px;
  border-radius: 50%;
  flex: none;
  background: var(--green);
  display: grid;
  place-items: center;
  color: #fff;
}
.dl-badge svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }

.sheet-offline { margin-top: 14px; display: flex; align-items: center; gap: 12px; }
.sheet-offline .dl-meta { flex: 1; }

/* ---------- post flow (§4) ---------- */

.screen--post { display: none; }
.screen--post.is-active { display: block; }
.post-step { position: absolute; inset: 0; }
.post-step[hidden] { display: none; }
.post-step--compose { overflow-y: auto; padding-bottom: 20px; }

.picker {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3px;
  padding: 0 3px;
}
.picker button {
  aspect-ratio: 1;
  border: 0; padding: 0;
  background: var(--tan) center/cover no-repeat;
  border-radius: 2px;
}

.post-topbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding: 46px 12px 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: linear-gradient(180deg, rgba(28,38,32,.55), rgba(28,38,32,0));
}
.post-topbar .btn--quiet { color: #fff; }
.accuracy-label {
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  background: rgba(28,38,32,.55);
  padding: 5px 10px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
}

/* floating photo thumbnail while the pin is being placed */
.post-thumb {
  position: absolute;
  top: 96px; left: 20px;
  width: 92px; height: 92px;
  border-radius: var(--r-md);
  background: var(--tan) center/cover no-repeat;
  border: 3px solid var(--paper);
  box-shadow: 0 6px 18px rgba(28,38,32,.3);
  transition: transform 320ms var(--ease), opacity 320ms var(--ease), border-radius 320ms var(--ease);
}
.post-thumb.is-snapping { border-radius: 50%; opacity: 0; }

/* translucent GPS-accuracy circle — radius = reported accuracy, at map scale */
.accuracy-circle {
  position: absolute;
  border-radius: 50%;
  background: rgba(232, 114, 76, .16);
  border: 1px solid rgba(232, 114, 76, .5);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* the pin being dropped/dragged: solid inside the accuracy circle, dashed
   outline outside it (§4's visual decouple) */
.drop-pin {
  position: absolute;
  width: 46px; height: 46px;
  transform: translate(-50%, -100%);
  touch-action: none;
  cursor: grab;
}
.drop-pin.is-dragging { cursor: grabbing; }

/* The resting/dragging visual is the flame mark, the same one the sign-in
   logo and every photo pin use — app.js appends it into .drop-pin-frame and
   into #intro-pin. Bottom-anchored, not top-anchored like .pin-frame: the
   mark is 1.287x as tall as the 46px box, and .drop-pin's
   translate(-50%,-100%) puts the box's bottom edge on the coordinate being
   pinned, which is where the tail point has to land. Since the mark's box is
   the cutout's own bounding box, bottom:0 now lands the tail exactly on that
   edge rather than the 0.9px short the vector frame's padded viewBox left.
   Centred rather than left-aligned for the same reason. The drop-settle
   keyframes are untouched either way. */
.drop-pin-frame > .flame-mark,
.intro-pin > .flame-mark {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  filter: drop-shadow(0 3px 10px rgba(28,38,32,.35));
}
/* "beyond GPS" (§4): the same three cues as before — faded fill, dashed
   outline, hole flipped to accent. A raster can't be stroked, so the dashed
   cue is a ring on the head circle (r .4990 of the image width, concentric
   with the hole) instead of following the licks and the tail, which now
   cross it. Flagged in the build note: it's the one behaviour the cutout
   can't reproduce exactly. */
.drop-pin.is-outside .flame-mark__img { opacity: .55; }
.drop-pin.is-outside .flame-mark__hole { background: var(--accent); }
.drop-pin.is-outside .flame-mark::after {
  content: '';
  position: absolute;
  left: 49.76%;
  top: 35.96%;
  width: 99.7%;
  height: calc(var(--pin-size) * .9225 * .997);
  transform: translate(-50%, -50%);
  border: 1.5px dashed var(--ink);
  border-radius: 50%;
}

/* Only the snap-to-photo transition now, not the pin's resting shape: hidden
   until .is-photo hands over to it, a beat before the real photoPin() draws. */
.drop-pin-body {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--tan) center/cover no-repeat;
  border: 2px solid var(--accent);
  box-shadow: 0 3px 10px rgba(28,38,32,.35);
  display: none;
}
.drop-pin.is-photo .drop-pin-frame { display: none; }
/* Position, not shape: the flame's hole sits .824 of the box height above the
   tail point, 14.9px higher than the old blob's centre. Without the shift the
   photo snaps *down* 15px and then jumps back up a beat later when the real
   photoPin() draws it in the hole. Size and treatment are untouched. The
   fraction moved from .8 to .824 with the cutout, because the real image sits
   its hole slightly higher in a slightly taller frame: (1 - .3596) * 1.2870. */
.drop-pin.is-photo .drop-pin-body {
  display: block;
  transform: translateY(calc(23px - 46px * .8241));
}

/* the one bespoke animation in the app (§4): drop and settle */
.drop-pin.is-dropping { animation: drop-settle 560ms var(--ease) both; }
@keyframes drop-settle {
  0%   { transform: translate(-50%, -100%) translateY(-140px) scale(.8); opacity: 0; }
  55%  { transform: translate(-50%, -100%) translateY(0) scale(1); opacity: 1; }
  72%  { transform: translate(-50%, -100%) translateY(-9px) scale(1.03); }
  86%  { transform: translate(-50%, -100%) translateY(0) scale(.99); }
  100% { transform: translate(-50%, -100%) translateY(0) scale(1); }
}
/* the contour ripple that spreads from the drop point (§1.3c) */
.drop-ripple {
  position: absolute;
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  transform: translate(-50%, -50%);
  animation: ripple 900ms var(--ease) both;
  pointer-events: none;
}
@keyframes ripple {
  from { opacity: .7; transform: translate(-50%, -50%) scale(.4); }
  to   { opacity: 0;  transform: translate(-50%, -50%) scale(4.4); }
}

.pin-status { margin: 0 0 12px; font-size: 13px; color: var(--slate); }
.pin-status.is-warning { color: var(--ink); }

.compose-pin { display: flex; justify-content: center; padding: 6px 0 18px; }
.field { display: block; padding: 0 20px 18px; border: 0; margin: 0; }
.field-label { display: block; font-size: 12px; color: var(--slate); margin-bottom: 6px; }
.field textarea {
  width: 100%;
  font: inherit;
  color: inherit;
  padding: 12px;
  border-radius: var(--r-md);
  border: 1px solid var(--hairline);
  background: var(--paper-raised);
  resize: none;
}
.friend-list { display: flex; flex-wrap: wrap; gap: 8px; }
.friend-chip {
  border: 1px solid var(--hairline);
  background: var(--paper-raised);
  border-radius: 999px;
  padding: 8px 14px;
  font-size: 14px;
}
.friend-chip[aria-pressed="true"] { background: var(--green); color: #fff; border-color: var(--green); }
.compose-actions { display: flex; justify-content: space-between; align-items: center; padding: 0 20px; }

/* ---------- feed (§5) ---------- */

.feed { padding: 0 16px; display: flex; flex-direction: column; gap: 22px; }
.card {
  background: var(--paper-raised);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  overflow: hidden;
}
.card-head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; }
.card-who { font-family: var(--font-head); font-weight: 600; font-size: 14px; }
.card-when { font-size: 12px; color: var(--slate); }
/* the identity row's avatar is the shared .avatar chip (friends §1) — the
   card no longer draws its own. The old single-photo `.card-lead` block is
   gone too: a hike card leads with the mini-map + lead-photo row (Blaze §2). */

/* map-context strip under the photo — a location is seen, not read */
.card-minimap {
  position: relative;
  display: block;
  width: 100%;
  height: 84px;
  border: 0;
  padding: 0;
  background: #EDE7D9;
  border-top: 1px solid var(--hairline);
  overflow: hidden;
}
.card-minimap svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.card-minimap .photo-pin { --pin-size: 34px; position: absolute; }
.card-caption { padding: 12px 14px 0; font-size: 14px; line-height: 1.45; }
.card-actions { display: flex; gap: 6px; padding: 8px 8px 12px; }
.card-actions button {
  border: 0; background: none;
  color: var(--slate);
  font-size: 13px;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  display: inline-flex; align-items: center; gap: 6px;
}
.card-actions button[aria-pressed="true"] { color: var(--green); font-weight: 600; }
.card-actions svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- empty states (§6): plainly true, no growth-loop pitch ---------- */

.empty {
  margin: 0 20px;
  padding: 26px 18px;
  border: 1px dashed var(--tan);
  border-radius: var(--r-md);
  color: var(--slate);
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
}

/* ---------- tab bar ---------- */

.tabbar[hidden] { display: none; }   /* the .tabbar display below outranks [hidden] */
.tabbar {
  flex: none;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  background: var(--green);
  padding: 8px 6px 26px;
  position: relative;
}
.tab {
  flex: 1;
  border: 0; background: none;
  color: rgba(247, 244, 236, .68);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  padding: 4px 0;
}
.tab svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.tab[aria-current="page"] { color: var(--paper); }
.tab--center { position: relative; }
.tab-fab {
  width: 52px; height: 52px;
  border-radius: 50%;
  background: var(--paper);
  color: var(--green);
  display: grid;
  place-items: center;
  margin-top: -24px;
  box-shadow: 0 6px 16px rgba(28, 38, 32, .35);
}
.tab-fab svg { width: 26px; height: 26px; stroke-width: 1.8; }

/* ================= friends-on-map extension =================
   ../designer/outputs/2026-08-17-hiking-app-friends-on-map-concept.md
   Zero new colors, one new component (the avatar chip), one new plain
   utility screen (Friends) + the first-run account capture. ========== */

/* ---------- avatar chip (friends §1) ----------
   ONE component, same as the photo pin: the map badge, the feed identity
   row, the friends list and the invite screen all render this markup at a
   different --avatar-size. Nothing else draws an avatar. */

.avatar {
  --avatar-size: 30px;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  flex: none;
  background: var(--tan) center/cover no-repeat;
  /* the chip's own 2px paper ring, so it stays legible on a photo
     thumbnail or a busy map — box-shadow, not border, so the ring never
     eats into the 40%-of-pin size ratio */
  box-shadow: 0 0 0 2px var(--paper);
  display: grid;
  place-items: center;
  overflow: hidden;
}
/* placeholder when the user skipped the photo step: solid palette tone +
   first initial. NOT sunrise-orange — that stays reserved for the pin. */
.avatar--initial {
  color: var(--paper);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: calc(var(--avatar-size) * .44);
  line-height: 1;
  letter-spacing: .01em;
  user-select: none;
}

/* The badge on a friend's map pin: bottom-right, overlapping the pin's edge
   by a third of the chip's own width.

   Derived from the two sizes rather than hard-coded against one ratio, so
   the 20px floor can bind on a small pin without throwing the overlap off:
   with head radius R and chip radius r, an overlap of chip/3 puts the chip's
   centre (R + r - chip/3) from the pin's centre along the 45° diagonal, i.e.
   (pin*.46 + chip/6) / √2 on each axis — .46 rather than .5 because the head
   circle is 92% of the frame's width now that the flame licks use the rest.

   Offsets are px off the centre rather than a percentage: a % here resolves
   against the pin's *padding* box, which the 3px accent border makes smaller
   than the pin. */
/* A.4: a filled paper plate behind the badge chip, on the same anchor, sized
   to the chip's full outer diameter (the dual-tone ring adds 3px all round)
   plus a 2px margin — big enough that no flame geometry pokes out from under
   the badge. No stroke or shadow on the plate itself; the chip's own ring
   draws on top of it unchanged. */
.photo-pin--badged > .pin-plate {
  position: absolute;
  left: calc(50% + (var(--pin-size) * .46 + var(--avatar-size) / 6) / 1.4142136);
  top: calc(50% + (var(--pin-size) * .46 + var(--avatar-size) / 6) / 1.4142136);
  transform: translate(-50%, -50%);
  width: calc(var(--avatar-size) + 10px);
  height: calc(var(--avatar-size) + 10px);
  border-radius: 50%;
  background: var(--paper);
}

.photo-pin--badged > .avatar {
  position: absolute;
  left: calc(50% + (var(--pin-size) * .46 + var(--avatar-size) / 6) / 1.4142136);
  top: calc(50% + (var(--pin-size) * .46 + var(--avatar-size) / 6) / 1.4142136);
  transform: translate(-50%, -50%);
  /* Dual-tone ring (badge-contrast rev. 2, DA-approved). A single ring could
     not clear both collisions at once: paper vanished against pale map and
     photos, trail-green vanished against the trail line it sits on. Two
     strokes in the same 3px footprint always leave one of them contrasting —
     2px forest-charcoal inside, 1px paper halo outside. */
  box-shadow: 0 0 0 2px var(--ink), 0 0 0 3px var(--paper);
}

/* ---------- profile → Friends row (friends §3) ---------- */

.row-link {
  display: flex;
  align-items: center;
  gap: 10px;
  width: calc(100% - 32px);
  margin: 0 16px 18px;
  padding: 14px 16px;
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  background: var(--paper-raised);
  text-align: left;
}
.row-link-label { flex: 1; font-family: var(--font-head); font-weight: 600; font-size: 16px; }
.row-link-meta { color: var(--slate); font-size: 14px; font-variant-numeric: tabular-nums; }
.row-link svg { width: 18px; height: 18px; fill: none; stroke: var(--slate); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- friends screen (friends §3) ---------- */

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin: 0 0 6px -6px;
  padding: 4px 6px;
  border: 0;
  background: none;
  color: var(--green);
  font-size: 15px;
  font-weight: 600;
}
.back-link svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }

.friends-block { padding: 0 20px; margin-bottom: 26px; }
.friends-h {
  font-size: 12px;
  font-weight: 600;
  color: var(--slate);
  margin: 0 0 8px;
  text-transform: none;
}
.rows { display: flex; flex-direction: column; }

.person-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--hairline);
}
.rows > .person-row:last-child { border-bottom: 0; }
.person-name { flex: 1; min-width: 0; font-size: 15px; }
.person-handle { display: block; font-size: 12px; color: var(--slate); }
.person-row .btn { padding: 7px 14px; font-size: 13px; }
/* "Requested": disabled, muted-gray label — not a second primary action */
.btn--requested { background: transparent; color: var(--slate); padding-inline: 6px; }

.add-row { display: flex; align-items: center; gap: 8px; }
.add-field, .text-input {
  font: inherit;
  color: inherit;
  padding: 11px 12px;
  border-radius: var(--r-md);
  border: 1px solid var(--hairline);
  background: var(--paper-raised);
}
.add-field { flex: 1; min-width: 0; }
.add-field::placeholder { color: var(--slate); }
.text-input { width: 100%; }

/* Username availability (friends §2, username addendum). Three states only:
   neutral while it is too short or still being checked, then available or
   taken. The note keeps its height reserved so the buttons below it do not
   jump as the state changes. */
.field-note {
  margin: 6px 2px 0;
  min-height: 16px;
  font-size: 12px;
  line-height: 16px;
  color: var(--slate);
}
.field-note.is-free { color: var(--green); }
/* muted per the approved spec — an unavailable username is a hint to try
   again, not a destructive action, so it does not borrow Sign Out's red */
.field-note.is-taken { color: var(--danger-muted); }
.btn--sm { padding: 10px 12px; font-size: 13px; white-space: nowrap; }

/* ---------- invite-link accept + first-run (friends §2/§3) ---------- */

.screen--center { display: none; place-items: center; padding: 24px; }
.screen--center.is-active { display: grid; }

.invite-card { text-align: center; max-width: 300px; }
.invite-avatar { display: flex; justify-content: center; margin-bottom: 18px; }
.invite-q {
  font-family: var(--font-head);
  font-size: 22px;
  line-height: 1.25;
  margin: 0 0 10px;
  letter-spacing: -.01em;
}
.invite-note { font-size: 13px; line-height: 1.5; color: var(--slate); margin: 0 0 22px; }
.invite-actions { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.invite-actions .btn--primary { min-width: 180px; }

/* deliberately unfinished: the not-signed-in invite case is flagged open in
   the concept (§5 item 5) and was not designed, so it is marked as a stub
   here rather than guessed at. */
.invite-stub {
  border: 1px dashed var(--tan);
  border-radius: var(--r-md);
  padding: 18px 16px;
  color: var(--slate);
  font-size: 13px;
  line-height: 1.5;
}

.onboard {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: var(--paper);
  display: flex;
  flex-direction: column;
}
.onboard[hidden] { display: none; }
/* min-height:0 is load-bearing: without it a flex item's automatic minimum
   size is its content, so this step grew past the phone instead of being the
   height of it — which is how the mood band's percentage and the form-steps'
   own scrolling both end up measured against the wrong number. */
.onboard-step { flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* step 1: mood treatment, then the two ways in. The mood band gave up half
   its height to the email/password form (Phase 2) rather than the form being
   pushed onto a screen of its own — one entrance screen, two doors. */
/* mood band, brand block, the two doors. Flex rather than a fixed row track:
   the form made this screen tall enough that a percentage mood band pushed
   the link row off the bottom of a 667pt phone. Here the doors keep their
   height, the brand block keeps its own, and the mood band gives up whatever
   is left over. */
.onboard-step[data-ob="signin"] {
  display: flex;
  flex-direction: column;
  padding: 0 24px 24px;
}
.onboard-mood { flex: 0 1 20%; min-height: 34px; }
/* mark + wordmark + squiggle move as one unit, centered in the space between
   the mood band and the doors — with the form added below, pinning them to
   the top of that space left a hole in the middle of the screen */
.onboard-brand {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.signin-doors { flex: 0 0 auto; }
.brand-mark { display: block; margin: 26px 0 0; }
.brand-mark .flame-mark { display: block; }
/* after the display rules above, or their specificity ties and beats it */
.onboard-step[hidden] { display: none; }
.onboard-mood { margin: 0 -24px; overflow: hidden; }
.onboard-contours { display: block; width: 100%; height: 100%; }
.onboard-contours path { fill: none; stroke: var(--slate); stroke-opacity: .2; stroke-width: 1.2; }
.onboard-wordmark {
  font-family: var(--font-head);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--green);
  margin: 12px 0 0;      /* was 30px; the B.2 mark now carries that space */
}
/* the squiggle under the wordmark (login concept §2) — the same element as
   the section-header rule, left-aligned to the wordmark it sits under and
   trimmed to roughly its width, so it reads as underlining that word rather
   than dividing the screen */
.contour-rule--brand { width: 112px; margin: 10px 0 0; }

/* ---------- the two doors (login concept §1) ----------
   Apple first and untouched, then the divider, then the form. The block sits
   at the bottom of the screen so the brand chrome above it keeps the top. */
.signin-doors { width: 100%; }

/* §1.4: a tan rule with a centered "or" — the divider the concept asks for,
   replacing the bare word this screen used before it had a real form. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 18px 0 6px;
  font-size: 12px;
  font-variant-caps: all-small-caps;
  letter-spacing: .06em;
  color: var(--slate);
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--tan);
}

/* ---------- email/password (Phase 2) ----------
   No new components beyond the underline field: the button is .btn--primary,
   the error line is the username field's own note style, and the mode switch
   is a text link rather than a second segmented control. */
.auth-form {
  width: 100%;
  display: grid;
  gap: 4px;
}
/* keeps its height when empty, so the button below never jumps as an error
   appears and clears */
.auth-form .field-note { margin: 2px 2px 6px; }

/* §1.5: underline fields, not boxed cards — tan at rest, thicker trail-green
   on focus. The thickening is drawn as a shadow rather than a wider border so
   the text never shifts by a pixel when focus moves. */
.field-line { position: relative; display: flex; align-items: center; }
.line-input {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--tan);
  border-radius: 0;
  padding: 12px 2px;
}
.line-input::placeholder { color: var(--slate); }
.line-input:focus {
  outline: none;                 /* replaced, not removed: the rule below is
                                    the visible focus indicator */
  border-bottom-color: var(--green);
  box-shadow: 0 1px 0 0 var(--green);
}
/* Edge draws its own reveal button inside password inputs; with the one
   below it that would be two controls doing the same thing. */
.line-input::-ms-reveal { display: none; }

/* 44px tall, like every other control here: the HIG minimum, and this one is
   right next to the field's own text so a small target would be worse than
   most. Vertically centered on the line rather than filling it. */
.reveal {
  position: absolute;
  right: 0;
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 2px 0 12px;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 13px;
  color: var(--slate);
}

/* §1.8: the inline error, directly under the field group, with a leading
   glyph. Masked rather than drawn so it takes the text's own color. */
.field-note--auth.is-error::before {
  content: "";
  display: inline-block;
  width: 13px;
  height: 13px;
  margin-right: 5px;
  vertical-align: -2px;
  background: currentColor;
  -webkit-mask: center/contain no-repeat var(--icon-alert);
  mask: center/contain no-repeat var(--icon-alert);
}
.field-note--auth.is-error { color: var(--danger-muted); }

/* §1.7: the secondary link row — mode switch left, "Forgot password?" right.
   Sign Up mode has no password to forget, so the row centers on the switch
   alone rather than leaving a gap where a link used to be. */
.auth-links {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 8px;
}
.auth-links.is-signup { justify-content: center; }
.link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;         /* HIG minimum; the text alone is ~16px */
  border: 0;
  background: transparent;
  padding: 0 2px;
  font: inherit;
  font-size: 13px;
  color: var(--green);
  font-weight: 600;
}
.link--quiet { color: var(--slate); font-weight: 400; }
.auth-switch { margin: 0; font-size: 13px; color: var(--slate); }
.onboard-body {
  margin: 0 0 20px;
  padding: 0 20px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--slate);
}
.onboard-body strong { color: var(--ink); font-weight: 600; }

/* OS-standard Sign in with Apple button (black variant per Apple's HIG) —
   deliberately not restyled into the app's palette; a familiar auth button
   is the point. */
.apple-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 50px;
  border: 0;
  border-radius: 10px;
  background: #000;
  color: #fff;
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -.01em;
}
.apple-btn svg { width: 20px; height: 20px; fill: currentColor; margin-top: -2px; }

/* step 2: confirm name, optional photo */
.onboard-step--form { padding: 74px 0 24px; overflow-y: auto; }
.onboard-title {
  font-family: var(--font-head);
  font-size: 28px;
  line-height: 1.15;
  letter-spacing: -.01em;
  margin: 0 0 8px;
  padding: 0 20px;
}
.onboard-step--form .contour-rule { margin: 0 20px 20px; }
.onboard-photo-row { display: flex; align-items: center; gap: 8px; padding: 0 20px; }
.picker--avatar { margin-top: 20px; }

/* ===================== Blaze Phase 1 =====================
   ../designer/outputs/2026-08-17-blaze-phase1-screens-concept.md
   No new palette, no new typeface, no new motif — the intro reuses the pin
   and its settle curve, the cards reuse the mini-map and thumbnails. ===== */

/* ---------- hike card header (§2) + Nearby badge (§3) ---------- */

.card-head { align-items: flex-start; }
.card-headtext { flex: 1; min-width: 0; }
.card-title {
  font-family: var(--font-head);
  font-size: 17px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -.01em;
  margin: 0 0 6px;
}
.card-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card-meta .card-who { font-size: 13px; font-weight: 600; }
.card-meta .card-when { font-size: 13px; color: var(--slate); }

/* the pin *shape* as an outline glyph, slate on tan — deliberately not
   sunrise-orange: proximity is metadata, not a capture/availability state */
.nearby {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  background: var(--tan);
  color: var(--ink);
  border-radius: 999px;
  padding: 3px 9px 3px 6px;
  font-size: 11px;
  font-weight: 600;
}
.nearby svg {
  width: 12px; height: 12px;
  fill: none; stroke: var(--slate);
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

.card-overflow {
  flex: none;
  border: 0; background: none;
  color: var(--slate);
  padding: 4px 2px 4px 8px;
  margin-top: -2px;
}
.card-overflow svg { width: 20px; height: 20px; fill: currentColor; display: block; }

/* ---------- lead-photo row + "+N" tile (§2) ----------
   Plain square thumbnails, NOT the circular-photo-pin crop: a gallery
   preview is a different object from a photo's map representation. */

.lead-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; padding: 3px 0 0; }
.lead-row:has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
.lead-row:has(> :only-child) { grid-template-columns: 1fr; }
.lead-thumb {
  aspect-ratio: 1;
  border: 0; padding: 0;
  background: var(--tan) center/cover no-repeat;
  border-radius: var(--r-sm);
  position: relative;
}
.lead-thumb--more::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: var(--r-sm);
  background: rgba(28, 38, 32, .55);
}
.lead-thumb--more span {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: var(--paper);
  font-family: var(--font-head);
  font-size: 22px;
  font-weight: 700;
  z-index: 1;
}

/* ---------- star rating (§7) ----------
   Trail-green filled, slate outline empty. No new color. */

.stars { display: flex; gap: 2px; padding: 12px 12px 0; }
.star { border: 0; background: none; padding: 2px; line-height: 0; }
.star svg { width: 20px; height: 20px; fill: none; stroke: var(--slate); stroke-width: 1.7; stroke-linejoin: round; }
.star.is-on svg { fill: var(--green); stroke: var(--green); }

/* ---------- photo dump (§2) ---------- */

.dump-count { margin: 0 0 4px; font-size: 13px; color: var(--slate); }
.dump { display: flex; flex-direction: column; gap: 4px; }
.dump-photo { width: 100%; aspect-ratio: 4 / 3; background: var(--tan) center/cover no-repeat; }

/* ---------- profile header + stats (§4) ---------- */

.screen-head .icon-btn {
  float: right;
  border: 0; background: none;
  color: var(--slate);
  padding: 4px;
  margin: -4px -4px 0 0;
}
.icon-btn svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

.profile-id { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; }
.profile-id h1 { margin: 0; }

.stats-row { display: flex; gap: 34px; padding: 4px 20px 20px; }
.stat-big {
  display: block;
  font-family: var(--font-head);
  font-size: 26px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  line-height: 1.1;
}
.stat-cap { font-size: 12px; color: var(--slate); }

/* ---------- segmented control (§5) ---------- */

.segmented {
  display: flex;
  gap: 2px;
  margin: 0 20px 18px;
  padding: 2px;
  background: rgba(201, 184, 150, .35);
  border-radius: 999px;
}
.segment {
  flex: 1;
  border: 0;
  border-radius: 999px;
  padding: 8px 0;
  background: none;
  color: var(--slate);
  font-size: 14px;
  font-weight: 600;
  font-family: var(--font-head);
}
.segment[aria-pressed="true"] { background: var(--paper-raised); color: var(--ink); box-shadow: 0 1px 3px rgba(28,38,32,.14); }

/* ---------- packing lists (§8) ----------
   Redefined per the packing-list concept: plain rows, no thumbnails. The
   overview reuses .row-link (the Friends row) rather than adding a card
   component — a packing list has no photo or location data to show. */

.lists { padding: 0 16px; display: flex; flex-direction: column; gap: 10px; }
.row-link--new { width: 100%; margin: 0; }
.row-link--new .row-link-label { color: var(--green); }
.lists .empty { margin: 2px 0 0; }

/* the checklist screen is a column: head, scrolling items, pinned add field.
   Sits after .screen.is-active on purpose — same specificity, later wins. */
.screen--pack.is-active { display: flex; flex-direction: column; }
.screen--pack .screen-head { flex: none; }

.pack-head { display: flex; align-items: flex-start; gap: 8px; }
.pack-head h1 { flex: 1; min-width: 0; margin-bottom: 2px; }
.pack-head .card-overflow { margin-top: 6px; }
/* a hike-linked list's name jumps to that hike; a planned trip's is inert */
/* `display: inline` so the chevron follows the last word of a wrapped title
   rather than being parked at the end of the flex line */
.pack-title-link {
  border: 0; background: none; padding: 0;
  font: inherit; color: inherit; text-align: left;
  display: inline;
}
.pack-title-link svg {
  width: 20px; height: 20px;
  display: inline-block;
  vertical-align: -2px;
  margin-left: 3px;
  fill: none; stroke: var(--slate); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.pack-count {
  margin: 0 0 8px;
  font-size: 13px;
  color: var(--slate);
  font-variant-numeric: tabular-nums;
}

.pack-items { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 0 8px; }
.pack-items .empty { margin: 12px 20px; }

/* swipe-to-delete: Delete sits under the row, the face slides off it */
.pack-row { position: relative; overflow: hidden; }
.pack-delete {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 78px;
  border: 0;
  background: var(--danger);      /* system-convention destructive red (§6) */
  color: #fff;
  font: inherit; font-size: 14px; font-weight: 600;
}
.pack-face {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  border: 0;
  background: var(--paper);
  padding: 13px 20px;
  font: inherit;
  color: inherit;
  text-align: left;
  border-bottom: 1px solid var(--hairline);
  touch-action: pan-y;            /* vertical scroll stays the browser's */
  transition: transform 180ms var(--ease);
}
.pack-row.is-dragging .pack-face { transition: none; }
.pack-row.is-open .pack-face { transform: translateX(-78px); }

.pack-box {
  flex: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1.6px solid var(--tan);
  display: grid;
  place-items: center;
  color: transparent;
  transition: background-color 140ms var(--ease), border-color 140ms var(--ease);
}
.pack-box svg {
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* packed: trail-green fill + strikethrough + slate text. Deliberately NOT
   sunrise-orange — packing isn't a capture/availability state (§1.1 rule). */
.pack-face[aria-pressed="true"] .pack-box {
  background: var(--green);
  border-color: var(--green);
  color: #fff;
}
.pack-face[aria-pressed="true"] .pack-text {
  color: var(--slate);
  text-decoration: line-through;
}
.pack-text { flex: 1; min-width: 0; font-size: 15px; }

.pack-add {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px 14px;
  border-top: 1px solid var(--hairline);
  background: var(--paper-raised);
}
.pack-add .add-field { flex: 1; min-width: 0; }

/* ---------- action sheet (§8) ---------- */

.sheet-scrim { position: absolute; inset: 0; z-index: 6; background: rgba(28, 38, 32, .38); }
.sheet--action {
  z-index: 7;
  max-height: 70%;
  overflow-y: auto;
}
.sheet--action .sheet-title { margin: 4px 0 10px; }
.sheet-row {
  display: flex;
  align-items: center;
  width: 100%;
  border: 0;
  background: none;
  padding: 13px 2px;
  font-size: 15px;
  text-align: left;
  border-bottom: 1px solid var(--hairline);
}
.sheet-row--add { color: var(--green); font-weight: 600; }
/* hike picker row: name left, the existing list's packed count right */
.sheet-row--pick { justify-content: space-between; gap: 12px; }
.sheet-row-label { min-width: 0; }
.sheet-row-meta {
  flex: none;
  color: var(--slate);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
/* system-convention exception, same as Settings' Sign Out (§6) */
.sheet-row--danger { color: var(--danger); font-weight: 600; }
.sheet--action .btn--wide { margin-top: 14px; }
.sheet--action .empty { margin: 4px 0 8px; }
.newlist { display: flex; flex-direction: column; gap: 12px; padding: 4px 0 2px; }

/* ---------- settings (§6) ---------- */

.group { padding: 0 20px; margin-bottom: 22px; }
.group-h { font-size: 12px; font-weight: 600; color: var(--slate); margin: 0 0 8px 2px; }
.group-box {
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  background: var(--paper-raised);
  overflow: hidden;
}
.set-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  border: 0;
  background: none;
  padding: 13px 14px;
  font-size: 15px;
  text-align: left;
  border-bottom: 1px solid var(--hairline);
}
.group-box > .set-row:last-child { border-bottom: 0; }
.set-label { flex: none; }
.set-trailing { margin-left: auto; color: var(--slate); font-size: 14px; display: flex; align-items: center; }
.set-chevron { width: 18px; height: 18px; flex: none; fill: none; stroke: var(--slate); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.set-input {
  margin-left: auto;
  min-width: 0;
  flex: 1;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: right;
  padding: 0;
}
/* Phase 2 stub: present, visibly inert, no tap action */
.set-row.is-disabled { color: var(--slate); opacity: .6; }
/* system-convention exception, not a brand color (§6) */
.set-row--danger .set-label { color: var(--danger); font-weight: 600; }

.toggle { margin-left: auto; display: flex; padding: 2px; gap: 2px; background: rgba(201,184,150,.4); border-radius: 999px; }
.toggle button {
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--slate);
  font-size: 13px;
  font-weight: 600;
  padding: 5px 13px;
  min-width: 44px;
}
.toggle button[aria-pressed="true"] { background: var(--green); color: var(--paper); }

/* ---------- app-intro load-in (§1) ---------- */

.intro {
  position: absolute;
  inset: 0;
  z-index: 8;
  background: var(--ink);       /* the existing dark surface, no new color */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 26px;
}
.intro-stage { position: relative; width: 200px; height: 120px; }
.intro-pin { left: 50%; top: 78px; }
.intro-ripple {
  position: absolute;
  left: 50%; top: 78px;
  width: 200px; height: 200px;
  transform: translate(-50%, -50%);
  opacity: 0;
}
.intro-ripple path { fill: none; stroke: var(--tan); stroke-width: 1.5; }
.intro-wordmark {
  font-family: var(--font-head);
  font-size: 34px;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--paper);
  margin: 0;
  opacity: 0;
}

/* Beat 1 reuses drop-settle, the literal curve from the pin moment — the
   same gesture, not a lookalike. Beats 2 and 3 follow on delays. */
.intro.is-playing .intro-pin { animation: drop-settle 560ms var(--ease) both; }
.intro.is-playing .intro-ripple { animation: intro-ring 520ms var(--ease) 500ms both; }
.intro.is-playing .intro-wordmark { animation: intro-word 400ms var(--ease) 800ms both; }
@keyframes intro-ring {
  from { opacity: .85; transform: translate(-50%, -50%) scale(.25); }
  to   { opacity: 0;   transform: translate(-50%, -50%) scale(1.5); }
}
@keyframes intro-word {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
/* reduced motion: the settled end frame, held, rather than the sequence */
.intro.is-static .intro-wordmark { opacity: 1; }

/* ---------- toast ---------- */

.toast {
  position: absolute;
  left: 20px; right: 20px; bottom: 96px;
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-md);
  padding: 12px 14px;
  font-size: 13px;
  text-align: center;
  animation: rise 200ms var(--ease) both;
}
@keyframes rise { from { opacity: 0; transform: translateY(8px); } }

/* ---------- real phone: drop the frame ---------- */

@media (max-width: 430px) {
  body { background: var(--paper); }
  .stage { display: block; min-height: 0; }
  .device {
    width: 100%; height: 100dvh;
    border-radius: 0;
    box-shadow: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}
