/* PlantMap viewer.
 *
 * Phone first. This is read in the field, one-handed, in daylight — so: large
 * tap targets, high contrast, and nothing that depends on hover. */

/* Palette pulled toward the basemap rather than sitting on top of it.
 *
 * The map is OpenFreeMap **positron** — near-greyscale, background rgb(242,243,240),
 * water rgb(194,200,202). The chrome is keyed to those greys so the page reads as
 * one surface. Colour is spent, deliberately, on exactly one thing: whether a
 * plant has been visited more than once. On a grey map a single accent carries
 * further than five competing ones. */

:root {
  color-scheme: light dark;
  --bg: #f2f3f0;          /* positron's own background */
  --surface: #ffffff;
  --fg: #1f2124;
  --muted: #74777c;
  --line: #dfe1de;
  --ink: #2b2e31;         /* markers and "+" marks: near-black, not a hue */
  --accent: #55585c;      /* restrained: greys the UI, never shouts */
  --accent-fg: #ffffff;
  --warm: #a8600f;        /* the ONE colour on the map — a revisited plant */
  --danger: #93332c;
  --shadow: 0 2px 10px rgba(31, 33, 36, .13);
  --radius: 12px;
  --new-data-stroke: #000000;
  /* The band the three navigation buttons occupy at the bottom of every page:
   * their offset from the bottom, plus their height (the 37px `min-height` set
   * on `#bottom-left button, .btn-link` further down). Anything else floating
   * over the bottom of the screen sits ABOVE this band, or it lands on top of
   * them — which on a phone means three buttons that cannot be tapped at all.
   * Both halves are tokens so the buttons and whatever dodges them cannot
   * drift apart. */
  --nav-gap: max(.8rem, env(safe-area-inset-bottom));
  --nav-band: calc(var(--nav-gap) + 37px);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #15171a;
    --surface: #1b1e22;
    --fg: #e6e7e4;
    --muted: #93969b;
    --line: #2b2f34;
    --ink: #e6e7e4;
    --accent: #9aa0a6;
    --accent-fg: #15171a;
    --warm: #d9a05a;
    --danger: #e58c85;
    --shadow: 0 2px 12px rgba(0, 0, 0, .55);
    --new-data-stroke: #ffffff;
  }
}

* { box-sizing: border-box; }

/* The `hidden` attribute is only a UA default of `display: none`, so ANY
 * `display` declaration on the element beats it. `.sheet { display: grid }` did
 * exactly that and the sign-in dialog rendered over the map on first load. This
 * rule makes `hidden` mean hidden for the four elements that toggle. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--fg);
  /* The CJK families are appended rather than substituted: `system-ui` already
     resolves to a Han-capable face on most devices, and the named fallbacks are
     for the ones where it does not — an Android build with a partial Noto, a
     Windows machine whose UI font has no Han coverage. Without them the browser
     picks per-glyph from whatever it can find, which is how a line of Chinese
     ends up in three different weights.
     Hant before Hans in each pair is not a preference: `<html lang>` decides
     which one the browser reaches for, and it is set before the first paint by
     lib/i18n.js. The order here only matters when lang is missing. */
  font: 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
        "PingFang TC", "PingFang SC", "Hiragino Sans GB",
        "Microsoft JhengHei", "Microsoft YaHei",
        "Noto Sans CJK TC", "Noto Sans CJK SC", "Source Han Sans", sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* TRACKING IS AN ALPHABET SETTING.
 *
 * Seven rules in this file add `letter-spacing` — six to small uppercase
 * labels, one negative on the brand wordmark — which
 * is right for Latin caps and wrong for Han: CJK glyphs are already full-width
 * squares set on a fixed grid, so extra tracking inserts a visible gap between
 * every single character and the label reads as if it has been stretched.
 * `text-transform: uppercase` needs no such guard — it is simply a no-op on Han.
 *
 * Every tracked rule below multiplies its own value by `--track`, so the six
 * different amounts are kept and switched off together rather than restated.
 * `<html lang>` is set by lib/i18n.js before the first paint, so this matches
 * from the first frame. */
:root { --track: 1; }
:root:lang(zh-Hans), :root:lang(zh-Hant) { --track: 0; }

/* The map is constructed at the hardcoded HOME and only moves to the visitor's
   own city when /api/geo answers, so it stays transparent until that opening
   view is settled — otherwise every visitor is shown Singapore for a moment
   and then a pan. app.js adds .map-ready on the geo answer or after its
   REVEAL_MS timeout, whichever comes first, so a hanging request cannot leave
   the page blank. */
#map {
  position: absolute; inset: 0;
  /* Track the DYNAMIC viewport height (mobile address bar shown/hidden) where
     supported; on engines without dvh the declaration is dropped and inset:0
     fills the layout viewport instead. app.js re-measures on viewport changes
     so a wrong first-frame size (the phone's half-map) never sticks. */
  height: 100dvh;
  opacity: 0;
  transition: opacity .25s ease;
}
#map.map-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  #map { transition: none; }
}

/* ------------------------------------------------------------------ chrome */
#chrome {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 15;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: .75rem;
  padding: max(.6rem, env(safe-area-inset-top)) .8rem .6rem;
  pointer-events: none;
}
#chrome > * { pointer-events: auto; }

/* Left side of the chrome: brand on top, filter buttons below it. */
.chrome-left {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .35rem;
}
/* Right side: just the username + cohort, top-aligned with the brand. */
.chrome-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}
/* Filter buttons below the brand, and the empty-map pill beside them. It wraps
 * rather than overflowing: on a narrow phone Filters + People + the pill is
 * wider than the space left by the account block opposite. */
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .45rem;
}
.actions button {
  font-size: .8rem;
  padding: .35rem .65rem;
  min-height: 36px;
}

.brand {
  padding: .55rem .95rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.brand strong {
  font-size: 1.35rem;
  letter-spacing: calc(-.02em * var(--track));
  line-height: 1.1;
}
/* English wordmark only ("brand" in lib/i18n.en.js is the one locale that
   splits "Pheno"/"Map" into markup — the Chinese strings are a different
   word, not a transliteration, and stay one uniform weight). */
.brand-thin { font-weight: 300; }

/* The brand and the message button sit in one row so the envelope aligns with
   the name rather than hanging off it. `.brand` keeps its own surface. */
.brand-row {
  display: flex;
  align-items: center;
  gap: .5rem;
}

/* The round envelope button, to the right of the Phenomap name. Same surface
   family as the brand so the row reads as one block. */
.msg-btn {
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;                      /* 80% of the old 1.25rem */
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--fg);
  box-shadow: var(--shadow);
  cursor: pointer;
  overflow: visible;                    /* don't clip the glyph */
}
.msg-btn:hover, .msg-btn:focus-visible { border-color: var(--accent); }

/* The red unread count, pinned to the button's top-right corner. */
.msg-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 20px;
  height: 20px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--danger);
  color: #ffffff;
  font-size: .68rem;
  font-weight: 700;
  line-height: 1;
  box-shadow: var(--shadow);
}
/* ------------------------------------------------------- corner readout */
/* Bottom-left block: stats readout and, when signed in, the "My species"
 * button. Wrapped so the stats sit at the very bottom when the button is
 * hidden rather than floating above an invisible control. */
#bottom-left {
  position: absolute;
  z-index: 4;
  left: 1rem;
  bottom: var(--nav-gap);
  display: flex;
  align-items: center;
  gap: .38rem;
  pointer-events: none;
  white-space: nowrap;
}
#bottom-left > * { pointer-events: auto; }

/* Copyright — signed-out map only. Bottom-RIGHT, under the zoom / locate
   controls: those are lifted to 4rem (see `.maplibregl-ctrl-bottom-right`
   below), so the strip beneath them is empty and this can sit at the true
   bottom of the corner without colliding with anything.

   The colour is hardcoded, NOT var(--muted). The basemap is always Positron
   whatever the app's theme is, so a token that lightens in dark mode would
   wash this out over a pale map. The white halo is the same reasoning as the
   pills above: unbacked grey text disappears over a park or a waterway. A
   backing pill was the alternative and was rejected — it would read as a
   button, in the one corner of this page where buttons live. */
#copyright {
  position: absolute;
  z-index: 4;
  right: 1rem;
  bottom: var(--nav-gap);
  margin: 0;
  text-align: right;
  max-width: min(28rem, calc(100% - 6rem));
  font-size: .78rem;
  line-height: 1.6;
  color: #6b6e72;
  text-shadow: 0 1px 2px rgba(255, 255, 255, .95),
               0 0 3px rgba(255, 255, 255, .9);
  /* The PARAGRAPH stays click-through so the map can be dragged from under it —
     it sits over the corner of a map and is not a control. The links inside it
     take their events back below. */
  pointer-events: none;
}

/* THE PRIVACY AND TERMS LINKS, WHICH DID NOT WORK UNTIL 2026-08-10.
   `pointer-events: none` above is inherited, so the two anchors were painted,
   underlined on hover by the browser's defaults, and absorbed nothing: every
   click went through to the map and panned it. Reported as "the links are not
   clickable", and the size was the red herring — a 0px hit area is not small,
   it is absent.

   Both halves of the fix are needed. `auto` gives them the click back; the
   inline-block padding gives them something to be clicked ON. At .69rem an
   inline anchor was about 10px tall, and this app's own rule everywhere else is
   that a control has to survive a thumb on a phone in a field (M9.11). The text
   is up to .78rem to match, and the underline is permanent rather than
   hover-only — hover does not exist on the device this is read on. */
#copyright a {
  pointer-events: auto;
  display: inline-block;
  padding: .3rem .1rem;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: .18em;
}
#copyright a:hover, #copyright a:focus-visible { color: #15171a; }

/* Signed in only. A pill button styled like the account name button so it reads
 * as part of the same family. */
/* All bottom-left navigation buttons — bold, for quick scanning. */
#bottom-left button {
  font-weight: 600;
}

.pill {
  display: inline-block;
  padding: .12rem .5rem;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: calc(.02em * var(--track));
  white-space: nowrap;
}
/* Cohort and status share one treatment, per Yilun: outlined, not filled. They
 * sit on the map rather than inside a card, so they carry their own light
 * backing — a 1px border alone disappears over a park or a waterway. The cohort
 * name is deliberately lighter than the user name so the two do not read as one
 * label. */
.pill-cohort,
.pill-live {
  background: rgba(255, 255, 255, .82);
  border: 1px solid rgba(31, 33, 36, .18);
  padding: .06rem .45rem;
}
.pill-cohort,
.pill-live { color: #9a9da2; }
/* Degraded state stays distinguishable WITHOUT going back to a filled block:
 * the outline and the type go warm instead. Losing that signal entirely would
 * make "the map stopped updating" undiagnosable from the page itself, which is
 * the M9.11 lesson. */
.pill-live[data-state="poll"] {
  color: var(--warm);
  border-color: color-mix(in srgb, var(--warm) 45%, transparent);
}

button {
  font: inherit;
  padding: .55rem .9rem;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--fg);
  box-shadow: var(--shadow);
  cursor: pointer;
}

/* ------------------------------------------------------------ account block */
/* Sits next to the brand on the left: a compact identity row that reads as
 * part of the chrome rather than a separate control. */
#acct {
  display: flex;
  align-items: flex-start;
  gap: .4rem;
}
#acct-tags {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .25rem;
  padding-top: .15rem;
  order: -1;               /* cohort to the left of the username */
}
#acct-wrap { position: relative; }

/* The name is the control. 120% of the default button scale so it is
   easy to tap and reads clearly against the brand. */
#userbtn {
  max-width: min(14rem, 35vw);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  font-size: .96rem;
  padding: .45rem .75rem;
  min-height: 40px;
}
#userbtn::after {
  content: " ▾";
  color: var(--muted);
  font-weight: 400;
}

#acct-menu {
  position: absolute;
  top: 100%;
  right: 0;
  min-width: 12rem;
  padding: .3rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
/* Right-aligned labels (Yilun, 2026-08-07): the panel hangs off the RIGHT edge
   of the username button, so ragged-left text lines the items up with the
   control that opened them and with the chrome's right edge. */
#acct-menu button {
  width: 100%;
  text-align: right;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  background: transparent;
  padding: .55rem .7rem;
  font-size: .92rem;
}
#acct-menu button:hover,
#acct-menu button:focus-visible { background: var(--bg); }
#signout { color: var(--danger); }
button.primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: transparent;
}
button:disabled { opacity: .55; cursor: default; }

/* ----------------------------------------------------------------- filters */
/* Anchored below the chrome on the left, opening downward from the
   filter buttons next to the brand. */
#filters {
  position: absolute;
  z-index: 16;
  top: 5.8rem;
  left: .8rem;
  width: min(20rem, calc(100vw - 1.6rem));
  max-height: min(32rem, calc(100vh - 6rem));
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: .9rem 1rem 1rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
}
/* On a narrow screen the panel spans the width rather than a 3-inch column. */
@media (max-width: 42rem) {
  #filters {
    top: auto;
    bottom: 0;
    left: .6rem;
    right: .6rem;
    width: auto;
    max-height: 65vh;
    border-radius: var(--radius) var(--radius) 0 0;
  }
}

/* ------------------------------------------------------------- intro card */
/* Signed-out only: what the "+" marks are, and why signing in shows more. See
   the block comment in index.html for why it exists at all.

   Same recipe as #filters and #people-filters — surface, radius, shadow, 1px
   line — and the same top-left anchor, because it occupies the slot the
   Filters panel will occupy once this visitor has an account. One box appears
   in one place on this page; which box depends only on whether you are signed
   in.

   z-index 14 puts it UNDER #chrome (15) and well under #signin-panel (30).
   That ordering is deliberate rather than incidental: on a phone the sign-in
   panel is `calc(100vw - 1.6rem)` wide and lands squarely on top of this, and
   it should — somebody who has pressed "Sign in" has finished reading the
   card. It is opaque, so nothing shows through. */
#intro {
  position: absolute;
  z-index: 14;
  /* NOT #filters' hardcoded 5.8rem, for two reasons.
     That figure clears the brand AND the 36px row of Filters/People buttons
     below it — but this card is only ever on screen when the ⓘ is hidden, so
     .actions is empty and has no height, and 5.8rem would leave a band of bare
     map between the brand and the card.
     The safe-area term is the second reason and the one #filters gets away
     with: it repeats the top padding on #chrome, so on a notched phone the
     card moves down with the brand instead of sliding under it. 3.2rem is the
     brand's own height (.55rem padding twice, plus 1.35rem at line-height
     1.1) plus .chrome-left's .35rem gap. */
  top: calc(max(.6rem, env(safe-area-inset-top)) + 3.2rem);
  left: .8rem;
  width: min(21rem, calc(100vw - 1.6rem));
  padding: .85rem 1rem 1rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
}
#intro h2 {
  margin: 0 1.75rem .35rem 0;   /* right margin clears the ✕ */
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.35;
}
#intro p {
  margin: 0;
  font-size: .88rem;
  line-height: 1.5;
  color: var(--fg);
}
/* The count sits apart from the prose: it is evidence, not another sentence,
   and at full weight beside two lines of body copy it read as a third one. */
#intro .intro-count {
  margin-top: .5rem;
  font-size: .78rem;
  color: var(--muted);
}
/* A 44px target, per this file's opening rule and the one #copyright a had to
   learn the hard way: a glyph is not a hit area. It is pulled into the card's
   own padding with negative offsets so the box does not grow a corner of empty
   space around it. */
#intro-close {
  position: absolute;
  top: .3rem;
  right: .3rem;
  min-height: 44px;
  min-width: 44px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  box-shadow: none;
  color: var(--muted);
  font-size: .95rem;
  line-height: 1;
}
#intro-close:hover, #intro-close:focus-visible { color: var(--fg); }

/* The ⓘ that the card collapses into. Sized like its neighbours in .actions
   (see that rule above) so it reads as one of the chrome's controls rather
   than a badge on the map, but square: it has a glyph where the others have a
   word, and the horizontal padding that fits "Filters" makes a lozenge of a
   single character. */
.info-mini {
  padding: .35rem;
  min-width: 36px;
  font-size: 1.05rem;
  line-height: 1;
}

/* Spans the width on a phone, like the two panels above — a 21rem column on a
   390px screen leaves a useless margin down one side.

   STILL TOP-ANCHORED, unlike those two. #filters and #people-filters become
   bottom sheets because they are opened deliberately and closed straight
   after; this one is on screen when the page loads. At the bottom it would
   land on the copyright strip and the Privacy and Terms links beside it —
   which are in that corner precisely because the people who need them are the
   signed-out ones looking at this card. */
@media (max-width: 42rem) {
  #intro {
    left: .6rem;
    right: .6rem;
    width: auto;
  }

  /* OUT OF THE WAY WHILE THE SIGN-IN PANEL IS OPEN, on a phone only.
     At this width the panel is `calc(100vw - 1.6rem)` and lands on top of the
     card — but a rem to the left of it, so a 10px ribbon of clipped Chinese
     stuck out down the side of the form and read as a layout fault. On a wide
     screen the two never meet: the panel hangs off the right of the chrome and
     the card is on the left, so this stays inside the media query rather than
     hiding the card on a desktop for no visible reason.

     Done in CSS rather than by toggling it from openSigninPanel(): it needs no
     state, so it cannot get stuck hidden, and the ONE way it can fail is a
     browser with no `:has()` — which lands back on the ribbon rather than on
     anything broken. #intro follows #chrome in the document, which is what
     makes the sibling combinator reach it. */
  #chrome:has(#signin-panel:not([hidden])) ~ #intro { display: none; }
}

#filters-open[aria-expanded="true"],
#taxa-open[aria-expanded="true"],
#people-open[aria-expanded="true"] {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: transparent;
}

/* People filter panel — admin only, same positioning as the other panels. */
#people-filters {
  position: absolute;
  z-index: 16;
  top: 5.8rem;
  left: .8rem;
  width: min(18rem, calc(100vw - 1.6rem));
  max-height: min(28rem, calc(100vh - 6rem));
  display: flex;
  flex-direction: column;
  padding: .9rem 1rem 1rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
}
#people-filters .f-group {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
#people-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
#people-chips:not(:empty) { margin-bottom: .4rem; }
#people-q {
  width: 100%;
  min-height: 40px;
  padding: .45rem .6rem;
  font: inherit;
  font-size: .9rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  flex: 0 0 auto;
}
#people-list {
  margin-top: .35rem;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
#people-list:empty { display: none; }
#people-filters .f-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: .5rem;
  margin: .3rem -1rem -1rem;
  padding: .6rem 1rem 1rem;
  background: var(--surface);
  flex: 0 0 auto;
}
#people-filters .f-actions button { flex: 1; box-shadow: none; min-height: 40px; font-size: .9rem; }

@media (max-width: 42rem) {
  #people-filters {
    top: auto; bottom: 0;
    left: .6rem; right: .6rem;
    width: auto;
    max-height: 65vh;
    border-radius: var(--radius) var(--radius) 0 0;
  }
}

/* Taxa filter panel — opens below the Taxa button on the left. */
#taxa-filters {
  position: absolute;
  z-index: 16;
  top: 5.8rem;
  left: .8rem;
  width: min(18rem, calc(100vw - 1.6rem));
  max-height: min(32rem, calc(100vh - 6rem));
  display: flex;
  flex-direction: column;
  padding: .9rem 1rem 1rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
}
#taxa-filters .f-group {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
}
#taxa-filters .f-group:last-of-type { flex: 1 1 auto; min-height: 4rem; }

/* Genus chips — same pattern as the people picker chips. */
#taxa-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
#taxa-chips:not(:empty) { margin-bottom: .4rem; }
.taxa-chip {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .18rem .3rem .18rem .55rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: .78rem;
  font-weight: 600;
}
.taxa-chip button {
  min-height: 0;
  padding: 0 .25rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  box-shadow: none;
  color: inherit;
  font-size: .9rem;
  line-height: 1;
  opacity: .8;
  cursor: pointer;
}

/* Search inputs — match the people-picker input. */
#taxa-genus-q,
#taxa-species-q {
  width: 100%;
  min-height: 40px;
  padding: .45rem .6rem;
  font: inherit;
  font-size: .9rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  flex: 0 0 auto;
}

/* Scrollable list areas. The genus list has a fixed cap; the species list
   fills the remaining panel height. */
#taxa-genus-list, #rec-taxa-genus-list {
  margin-top: .35rem;
  max-height: 8rem;
  overflow-y: auto;
}
#taxa-species-list, #rec-taxa-species-list {
  margin-top: .35rem;
  max-height: 14rem;
  overflow-y: auto;
}
#taxa-genus-list:empty, #rec-taxa-genus-list:empty,
#taxa-species-list:empty,
#rec-taxa-species-list:empty { display: none; }

/* Genus row — same pattern as .f-person. */
.taxa-genus-row {
  display: flex;
  align-items: center;
  gap: .45rem;
  width: 100%;
  padding: .4rem .5rem;
  min-height: 38px;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  background: transparent;
  text-align: left;
  font: inherit;
  font-size: .88rem;
  font-weight: 600;
  font-style: italic;
  cursor: pointer;
}
.taxa-genus-row:hover,
.taxa-genus-row:focus-visible { background: var(--bg); }
.taxa-genus-row[aria-selected="true"] { color: var(--accent); }
.taxa-genus-row .tick { width: 1rem; color: var(--accent); font-size: .8rem; }

/* Species row — same pattern, indented. */
.taxa-species-row {
  display: flex;
  align-items: center;
  gap: .45rem;
  width: 100%;
  padding: .35rem .5rem;
  min-height: 36px;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  background: transparent;
  text-align: left;
  font: inherit;
  font-size: .84rem;
  font-style: italic;
  cursor: pointer;
}
.taxa-species-row:hover,
.taxa-species-row:focus-visible { background: var(--bg); }
.taxa-species-row[aria-selected="true"] { font-weight: 600; }
.taxa-species-row .tick { width: 1rem; color: var(--accent); font-size: .8rem; }

#taxa-filters .f-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: .5rem;
  margin: .3rem -1rem -1rem;
  padding: .6rem 1rem 1rem;
  background: var(--surface);
  flex: 0 0 auto;
}
#taxa-filters .f-actions button { flex: 1; box-shadow: none; min-height: 40px; font-size: .9rem; }

/* Mobile: taxa panel as a bottom sheet. */
@media (max-width: 42rem) {
  #taxa-filters {
    top: auto;
    bottom: 0;
    left: .6rem;
    right: .6rem;
    width: auto;
    max-height: 65vh;
    border-radius: var(--radius) var(--radius) 0 0;
  }
}

/* The date and scope groups keep their natural height; only the people group
 * gives, because only it contains a list that can be any length. */
.f-group { margin-bottom: .95rem; flex: 0 0 auto; }
.f-group:last-of-type { margin-bottom: .6rem; }
#f-people-group {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 6rem;
}
.f-legend {
  display: block;
  margin-bottom: .35rem;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: calc(.06em * var(--track));
  text-transform: uppercase;
  color: var(--muted);
}

.f-dates {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: .4rem .5rem;
}
.f-dates label { font-size: .82rem; color: var(--muted); }
.f-dates input {
  width: 100%;
  min-height: 40px;
  padding: .4rem .55rem;
  font: inherit;
  font-size: .9rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
}

/* Segmented control. One of the two is always `aria-pressed`, so the state is
 * readable without relying on the colour alone. */
#f-who { display: flex; gap: 0; }
#f-who button {
  flex: 1;
  min-height: 40px;
  padding: .4rem .6rem;
  font-size: .88rem;
  border-radius: 0;
  box-shadow: none;
  background: var(--bg);
  color: var(--muted);
}
#f-who button:first-child { border-radius: 999px 0 0 999px; }
#f-who button:last-child  { border-radius: 0 999px 999px 0; border-left: 0; }
#f-who button[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: transparent;
  font-weight: 600;
}

/* The people picker. A selection here OVERRIDES the Mine/Everyone control
 * above, so the chips have to be visible whenever any are set — a hidden
 * selection silently narrowing the map is exactly the kind of invisible filter
 * this project has been bitten by before. */
#f-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
#f-chips:not(:empty) { margin-bottom: .4rem; }
.f-chip {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .18rem .3rem .18rem .55rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: .78rem;
  font-weight: 600;
}
.f-chip button {
  min-height: 0;
  padding: 0 .25rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  box-shadow: none;
  color: inherit;
  font-size: .9rem;
  line-height: 1;
  opacity: .8;
}
.f-chip button:hover { opacity: 1; }

#f-person-q {
  width: 100%;
  min-height: 40px;
  padding: .45rem .6rem;
  font: inherit;
  font-size: .9rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
}
#f-person-list {
  margin-top: .35rem;
  flex: 1 1 auto;
  min-height: 0;          /* or a flex child refuses to shrink below its content */
  overflow-y: auto;
}
#f-person-q { flex: 0 0 auto; }
#f-person-list:empty { display: none; }
.f-person {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .45rem .5rem;
  min-height: 40px;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  background: transparent;
  text-align: left;
  font-size: .88rem;
}
.f-person:hover,
.f-person:focus-visible { background: var(--bg); }
.f-person[aria-selected="true"] { font-weight: 600; }
.f-person .tick { width: 1rem; color: var(--accent); }
.f-person .who { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.f-person .lead {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: calc(.04em * var(--track));
  text-transform: uppercase;
  color: var(--muted);
}
.f-empty { padding: .45rem .5rem; font-size: .84rem; color: var(--muted); }

/* Pinned to the bottom of the scrolling panel, not sitting at the end of it.
 *
 * Found by a click that reported success and did nothing: as a LEAD the panel
 * grows a whole extra section — the people picker — and on a laptop viewport
 * Clear and Done end up below the panel's own scroll line. The controls that
 * undo a filter must never be the ones you have to scroll to find, and this is
 * the role most likely to have set a filter worth undoing.
 *
 * The negative margins let the background span the panel's full width, so the
 * list scrolls *under* the bar rather than appearing beside it. */
.f-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: .5rem;
  margin: .3rem -1rem -1rem;
  padding: .6rem 1rem 1rem;
  background: var(--surface);
}
.f-actions button { flex: 1; box-shadow: none; min-height: 40px; font-size: .9rem; }

/* ------------------------------------------------------------------ notice */
/* Failures only now — the empty map states itself in the chrome, beside the
   Filters button. Lifted clear of --nav-band: at this width it spans the whole
   of a phone screen, and it used to land squarely on the three navigation
   buttons and eat their taps. */
#notice {
  position: absolute;
  z-index: 4;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--nav-band) + .5rem);
  width: min(30rem, calc(100vw - 1.6rem));
  padding: .8rem 1rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  font-size: .88rem;
  line-height: 1.45;
}
#notice strong { display: block; margin-bottom: .15rem; }
#notice.err { border-left: 4px solid var(--danger); }

/* ----------------------------------------------------------- message toast
 * The on-screen notice when a message arrives (lib/messages.js). Same corner,
 * same lift above the nav band as #notice, but it is a LINK to /messages and
 * it auto-dismisses, so it gets the accent treatment a call-to-action should
 * have rather than the neutral surface of a status line. One at a time, fading
 * in on entry and out on dismissal. */
.toast {
  position: fixed;
  z-index: 30;
  left: 50%;
  transform: translateX(-50%) translateY(12px);
  bottom: calc(var(--nav-band) + .5rem);
  width: min(30rem, calc(100vw - 1.6rem));
  padding: .8rem 1rem;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-fg);
  box-shadow: var(--shadow);
  font-size: .88rem;
  line-height: 1.45;
  opacity: 0;
  transition: opacity .25s ease, transform .25s ease;
}
.toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; }
}
.toast a, .toast a:visited { color: var(--accent-fg); font-weight: 600; }
.toast-close {
  position: absolute;
  top: .25rem;
  right: .35rem;
  border: 0;
  background: none;
  box-shadow: none;
  color: inherit;
  font-size: 1.1rem;
  padding: .15rem .4rem;
  min-height: 0;
  cursor: pointer;
}

/* -------------------------------------------------------------- sign-in sheet */
.sheet {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 1rem;
  background: rgba(10, 12, 10, .45);
  backdrop-filter: blur(2px);
}
.sheet form, .sheet-form {
  width: min(32rem, 100%);
  max-height: 100%;
  overflow-y: auto;
  padding: 1.3rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.sheet h2 { margin: 0 0 .4rem; font-size: 1.2rem; }
.sheet label {
  display: block;
  margin: .9rem 0 .25rem;
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
}
.sheet input {
  width: 100%;
  padding: .7rem .8rem;
  min-height: 44px;
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.sheet .row { display: flex; gap: .5rem; margin-top: 1.1rem; }
.sheet .row button { flex: 1; box-shadow: none; }

/* .error is still live — it carries the sign-in failure message, which is the
 * only text left in this dialog besides the labels. */
.error { color: var(--danger); font-size: .86rem; margin: .8rem 0 0; }

/* The "you're offline" banner (OFFLINE_PLAN §5a). A notice, not an error:
 * recording still works, and the record will be kept on this phone and sent
 * later. Used on the wizard, My Species and the outbox page. */
.offline-banner {
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--warm);
  border-radius: 8px;
  padding: .7rem .9rem;
  margin: 1rem 0;
  font-size: .9rem;
}

/* One queued record on /outbox — a stacked block (the .rec-row pattern is a
 * flex row for clickable list rows; these carry their own buttons). */
.outbox-item {
  padding: .7rem .8rem;
  margin-bottom: .5rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
}
.outbox-item strong { display: block; font-size: .95rem; margin-bottom: .1rem; }
.outbox-item .p-meta { margin-top: .15rem; }
.outbox-item-actions {
  display: flex; gap: .75rem; margin-top: .6rem;
}
.outbox-item-actions button { min-height: 34px; }

/* ------------------------------------------------ recipient picker listbox */
/* /send-message's recipient picker. A native <option> cannot style part of its
   text and display names are not unique, so the picker is a button + listbox
   showing each member's name with the email at half size and grey beneath it —
   the disambiguator. Pattern mirrors #lang-menu (lib/language.js). */
.recipient { position: relative; }
.recipient-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
.recipient-chips:not(:empty) { margin-bottom: .4rem; }
.recipient-btn {
  width: 100%;
  padding: .7rem .8rem;
  min-height: 44px;
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  text-align: left;
  box-shadow: none;
  cursor: pointer;
}
.recipient-caret { color: var(--muted); }
.recipient-list {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0; right: 0;
  max-height: 16rem;
  overflow-y: auto;
  padding: .3rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
.recipient-list button {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  background: transparent;
  color: var(--fg);
  font: inherit;
  padding: .5rem .7rem;
  cursor: pointer;
}
.recipient-list button:hover,
.recipient-list button:focus-visible { background: var(--bg); }
.recipient-list button[aria-selected="true"] { background: var(--bg); font-weight: 600; }
.recipient-list .tick { width: 1rem; flex: 0 0 auto; padding-top: .1rem; color: var(--accent); }
.recipient-text { flex: 1; min-width: 0; }
.recipient-name { display: block; }
.recipient-email { display: block; font-size: .5em; color: var(--muted); line-height: 1.3; }
.recipient-none { padding: .5rem .7rem; font-size: .86rem; color: var(--muted); }

/* ------------------------------------------------------------------- panel */
#panel {
  position: fixed;
  z-index: 10;
  background: var(--surface);
  box-shadow: var(--shadow);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* Phone: a bottom sheet. Desktop: a right-hand rail. */
@media (max-width: 42rem) {
  #panel {
    left: 0; right: 0; bottom: 0;
    max-height: 72vh;
    border-radius: var(--radius) var(--radius) 0 0;
    padding: 1.1rem 1rem max(1.1rem, env(safe-area-inset-bottom));
  }
}
@media (min-width: 42.01rem) {
  #panel {
    /* Starts below the chrome rather than under it, so the close button and the
     * Sign out button never occupy the same corner. */
    top: 3.9rem; right: 0; bottom: 0;
    width: 24rem;
    padding: 1.2rem 1.2rem 2rem;
    border-left: 1px solid var(--line);
    border-top-left-radius: var(--radius);
  }
}
#panel-close {
  position: absolute;
  top: .6rem; right: .6rem;
  width: 40px; height: 40px;
  min-height: 40px;
  padding: 0;
  border-radius: 999px;
  box-shadow: none;
}

.p-name { margin: .2rem 3rem .1rem 0; font-size: 1.15rem; line-height: 1.25; }
/* The heading IS the botanical name now — common names are no longer rendered
 * anywhere — so the italic that used to belong to a separate `.p-sci` line
 * moves onto the heading itself. Applied by a class rather than to `.p-name`
 * outright, because the fallback text for a plant with no scientific name is
 * not a binomial and must not be set in italic. */
.p-name.sci { font-style: italic; }
.p-meta { margin: .5rem 0 0; font-size: .8rem; color: var(--muted); }
.p-meta.warn { color: var(--danger); }
.p-meta code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .92em;
}

.visits-head {
  margin: 1.3rem 0 .1rem;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: calc(.06em * var(--track));
  text-transform: uppercase;
  color: var(--muted);
}
.visits-note { margin: .1rem 0 .8rem; font-size: .82rem; color: var(--warm); }

.visit {
  padding: .85rem 0;
  border-top: 1px solid var(--line);
}
.visit-head-row { display: flex; align-items: center; justify-content: space-between; gap: .4rem; }
.visit-when { font-weight: 600; font-size: .95rem; }
.visit-edit {
  flex-shrink: 0; min-width: 36px; min-height: 36px; padding: .25rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); font-size: .85rem; box-shadow: none;
  cursor: pointer; line-height: 1;
}
.visit-edit:hover { border-color: var(--accent); }
.visit-sub  { font-size: .78rem; color: var(--muted); margin-top: .1rem; }

.shots { display: flex; gap: .4rem; flex-wrap: wrap; margin: .6rem 0 0; }
.shots img {
  width: 84px; height: 84px;
  object-fit: cover;
  border-radius: 10px;
  background: var(--line);
  cursor: zoom-in;
}

.scores { display: flex; flex-wrap: wrap; gap: .3rem; margin: .6rem 0 0; }
.score {
  font-size: .75rem;
  padding: .18rem .5rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}
.score.yes { background: var(--ink); color: var(--surface); border-color: transparent; }
.score.unsure { border-color: var(--warm); color: var(--warm); }
.scores-none { font-size: .78rem; color: var(--muted); margin: .5rem 0 0; }

/* Observation-level tags — cut flower, indoor. Same pill family as .score
   but outlined so they read as metadata rather than data. */
.obs-tags { display: flex; flex-wrap: wrap; gap: .3rem; margin: .4rem 0 0; }
.obs-tag {
  display: inline-block;
  font-size: .72rem;
  padding: .12rem .45rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}
.obs-tag.cut-flower { border-color: var(--warm); color: var(--warm); }
.obs-tag.indoor     { border-color: var(--accent); color: var(--accent); }

/* ---------------------------------------------------------------- lightbox */
#lightbox {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: 1rem;
  background: rgba(8, 9, 8, .92);
  cursor: zoom-out;
}
#lightbox img {
  max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 2rem);
  object-fit: contain;
  border-radius: 8px;
}

/* Popup-free map: clicks open the panel instead, so nothing is styled here. */
.maplibregl-ctrl-attrib { font-size: 10px; }

/* Push the zoom / locate controls up so they don't overlap the
   bottom-left navigation buttons. */
.maplibregl-ctrl-bottom-right { bottom: 4rem; }

/* The location-picker map on the New Observation page is a self-contained
   widget — its controls sit at the natural bottom-right of the map div. */
#location-map .maplibregl-ctrl-bottom-right { bottom: 0; }

/* Sign-in button sits in the same right-top slot as the username button
   and should match its scale. */
#signin-open {
  font-size: .96rem;
  padding: .45rem .75rem;
  min-height: 40px;
}

/* The signed-out pair. .chrome-right is a column, so without this row the two
   buttons stack and the language control lands ABOVE "Sign in" rather than to
   its left. */
#signedout-actions {
  display: flex;
  align-items: center;
  gap: .4rem;
}

/* The signed-out language button, left of Sign in and quieter than it: this is
   the secondary control in that slot, and the primary one is the reason the
   visitor is there. Still 40px tall — it is tapped on a phone like everything
   else, and a control that only works at a desk fails exactly when it matters
   (the same lesson as the geolocation button, M9.11). */
.lang-mini {
  font-size: .9rem;
  padding: .45rem .6rem;
  min-height: 40px;
  color: var(--muted);
}

/* --------------------------------------------------------------- language */
/* The same box as #acct-menu, because it is the same kind of thing: a short
   list hanging off the control that opened it (Yilun, 2026-08-08). It was a
   full-screen dialog with radios and an Apply button, which was three taps and
   a modal over the map to answer a question with three answers.

   `fixed`, not `absolute`: the chrome is fixed, the box is placed from its
   anchor's viewport rect by lib/language.js, and fixed means no scroll maths
   and no chance of the box drifting away from its button. */
#lang-menu {
  position: fixed;
  z-index: 30;
  min-width: 10rem;
  padding: .3rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
#lang-menu button {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 8px;
  box-shadow: none;
  background: transparent;
  /* Taller than an account-menu row. This one is read by somebody who may be
     picking by the shape of the characters rather than by reading them, and it
     is the control a person reaches for when they are already lost. */
  padding: .6rem .7rem;
  /* 简体中文 and 繁體中文 share most of their codepoints and differ in stroke
     count; at caption size they are genuinely hard to tell apart, and telling
     them apart is this control's entire job. */
  font-size: 1rem;
}
#lang-menu button:hover,
#lang-menu button:focus-visible { background: var(--bg); }
#lang-menu button[aria-checked="true"] { font-weight: 600; }
#lang-menu button:disabled { opacity: .5; }
/* Its own cell, present in every row whether ticked or not, so the labels line
   up instead of shifting sideways as the choice moves. */
.lang-tick {
  width: 1em;
  flex: none;
  color: var(--muted);
}

/* When the brand is a link (My Species page), keep it looking like the wordmark. */
a.brand {
  text-decoration: none;
  color: inherit;
}

/* Map link button in the chrome — same shape as the sign-in button. */
.btn-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: .55rem .9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--fg);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--shadow);
  cursor: pointer;
}

/* The three bottom navigation buttons run at 85% of the standard control
   scale, per Yilun — they are always on screen and should not compete with
   the map or the page body. Both spellings are sized here in one place: the
   map page renders them as <button>s in #bottom-left, every other page as
   .btn-link anchors in #species-map-btn-wrap. */
#bottom-left button,
.btn-link {
  font-size: .85rem;      /* 85% of the 16px base */
  padding: .47rem .765rem;
  min-height: 37px;
}

/* "New data" button gets a contrasting stroke so it stands out
   from the other navigation pills — black on light, white on dark. */
#new-observation-open,
a.btn-link[href="/new-observation"] {
  border: 3px solid var(--new-data-stroke);
}

/* Current-page indicator: greyed out, not clickable. On the New Data
   page the stroke is greyed as well so it is not the only active element. */
.btn-link.current {
  opacity: .4;
  pointer-events: none;
}
a.btn-link[href="/new-observation"].current {
  border-color: var(--muted);
}

/* ================================================================== species */
body.species-page {
  height: auto;
  overflow-y: auto;
}

#species-content {
  padding-top: 5rem;
  padding-bottom: 5rem;
  max-width: 56rem;
  margin: 0 auto;
  padding-left: 1rem;
  padding-right: 1rem;
}

#species-loading {
  text-align: center;
  padding: 3rem 1rem;
  color: var(--muted);
  font-size: .95rem;
}

/* Summary tiles — totals above the list or chart, never affected by filters.
   The row runs the full content width with equal columns, the same on both
   pages. It is deliberately NOT inset to line up with the chart columns on My
   Species: the two pages showing one tile row in one shape beats each row
   agreeing with whatever sits under it. */
#species-summary {
  margin-bottom: 1.1rem;
}
.summary-row {
  display: flex;
  gap: .6rem;
}
.summary-tile {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: .55rem .5rem .45rem;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--line);
}
.summary-tile b {
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1.15;
}
.summary-tile span {
  font-size: .72rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: calc(.04em * var(--track));
  margin-top: .1rem;
}
/* "5/20" on the species tile — the leading count is the number being read,
   the total behind the slash steps back. Scoped inside the <b> to undo the
   label styling above, which would otherwise catch this nested span and
   shrink it to caption size. */
.summary-tile b span {
  font-size: inherit;
  letter-spacing: normal;
  text-transform: none;
  margin-top: 0;
  color: var(--muted);
  font-weight: 600;
}

/* What the leading number on the species tile counts. The tile carries the
   same sentence as a title attribute, but a tooltip is unreachable on a touch
   screen and this is the only place in the UI that states the rule. */
.summary-note {
  margin: .45rem 0 0;
  font-size: .72rem;
  line-height: 1.35;
  color: var(--muted);
}

/* A gentle separator between summary tiles and the chart. */
#species-chart-area {
  border-top: 1px solid var(--line);
  padding-top: 1rem;
}

/* All Records has no chart, so its toolbar has nothing to align to and the
   left gutter collapses. The summary row needs no override — it is full width
   on both pages. */
.records-page #species-toolbar {
  padding-left: 0;
}

/* Toolbar row: buttons left, year nav right, aligned with the chart. */
#species-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: .8rem;
  padding-left: var(--sp-left);
}
.toolbar-left {
  display: flex;
  align-items: center;
  gap: .4rem;
}
.toolbar-left button {
  min-height: 34px;
  padding: .35rem .7rem;
  font-size: .82rem;
  box-shadow: none;
}
.toolbar-right {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding-right: calc(3rem + .35rem);
}

/* Year navigation (now inside the toolbar). */
.year-btn {
  min-height: 34px;
  padding: .25rem .6rem;
  font-size: .88rem;
  box-shadow: none;
}
.year-label {
  font-size: 1.05rem;
  font-weight: 700;
  min-width: 3.5rem;
  text-align: center;
}

/* Month header — aligns with the chart, after the plant-names column. */
.month-row {
  display: flex;
  align-items: flex-end;
  gap: 0;
  margin-bottom: .35rem;
}
.month-offset {
  flex: 0 0 calc(var(--sp-left) + .55rem + 1px + 3rem + .35rem);
}
.month-cells {
  flex: 1;
  display: flex;
}
.month-cell {
  flex: 1;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: calc(.03em * var(--track));
  text-transform: uppercase;
  color: var(--muted);
}

/* Turned on its side, reading bottom-to-top — twelve three-letter months will
   not fit across a phone otherwise, and a rotated label is the standard answer
   on a crowded axis.

   `writing-mode` rather than `transform: rotate(-90deg)`: a transform is purely
   visual, so the row would reserve the label's HORIZONTAL width and the text
   would sit outside its own box, overlapping the neighbours it was rotated to
   avoid. Vertical writing mode is real layout — the row grows to the height of
   the longest month by itself, with no magic constant to keep in sync. The
   180° rotation flips vertical-rl's top-to-bottom into bottom-to-top. */
/* Turned on their side ONLY in English.
 *
 * The rotation exists because three Latin letters ("Sep") do not fit this
 * column width. The Chinese labels are two characters ("9月") and do fit, and a
 * rotated Han character is not a tight label — it is a character lying on its
 * back, which is simply wrong. my-species.js takes the month names from Intl,
 * so the text really is shorter here; this rule follows it. */
.month-label {
  line-height: 1;
}
:lang(en) .month-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}

/* Width of the plant-names column + separator, shared by month-row and
   plant-row so the charts all line up. */
:root {
  --sp-left: 9rem;
}

/* One plant block. */
.plant-card {
  margin-bottom: 1.4rem;
  padding: .65rem 0;
  border-top: 1px solid var(--line);
}

/* Side-by-side: plant names | chart. */
.plant-row {
  display: flex;
  align-items: flex-start;
  gap: 0;
}

/* Plant names — right-aligned, two lines, flush with the top of the chart. */
.plant-names {
  flex: 0 0 var(--sp-left);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  padding-right: .55rem;
  padding-top: .1rem;        /* optical alignment with the first organ row */
  overflow: hidden;
}
.plant-common {
  font-weight: 600;
  font-size: .88rem;
  line-height: 1.25;
  text-align: right;
}
.plant-sci {
  font-style: italic;
  font-size: .8rem;
  line-height: 1.25;
  color: var(--muted);
  text-align: right;
}
.plant-names.sci-only .plant-sci {
  font-size: .88rem;
  color: var(--fg);
  font-weight: 600;
}

/* Vertical rule between names and chart. */
.plant-sep {
  flex: 0 0 1px;
  align-self: stretch;
  background: var(--line);
  margin-right: .55rem;
}

/* Chart area — the organ rows. */
.plant-chart {
  flex: 1;
  min-width: 0;
}

/* One organ row. */
.organ-row {
  display: flex;
  align-items: center;
  gap: 0;
  margin-bottom: .4rem;
}
/* A line drawing, not a word — see lib/organ-icons.js. The 3rem column width
   is load-bearing: .month-offset and .toolbar-right both add it into their
   calc() so the header and the year nav line up with the bars. Resize the
   icon freely; change the COLUMN width and all three must move together. */
.organ-label {
  flex: 0 0 3rem;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: var(--muted);
  padding-right: .35rem;
}
/* Sized well inside the 28px .timeline it sits against, so the icon reads as
   a label beside the bar rather than another thing of bar height. The artwork
   is filled outline geometry with no stroke, so scale is the only knob — and
   the 200x200 viewBox means any size is lossless. */
.organ-label svg {
  display: block;
  width: 1.35rem;
  height: 1.35rem;
}

/* The 12-month timeline bar. */
.timeline {
  flex: 1;
  position: relative;
  height: 28px;
  background: var(--bg);
  border-radius: 6px;
}

/* Month dividers — thin vertical lines. */
.tl-month {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--line);
  pointer-events: none;
}

/* One observation point — clickable now. */
.tl-point {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 11px;
  height: 11px;
  border-radius: 999px;
  cursor: pointer;
}

/* Leaf colours. */
.tl-point.leaf-emerging { background: #88b888; }
.tl-point.leaf-full     { background: #2d6a2d; }
.tl-point.leaf-dropping { background: #8b6914; }

/* Flower colours. */
.tl-point.flower-pre  { background: #f4a7c0; }
.tl-point.flower-full { background: #c04d82; }

/* Fruit — one grey for everything. */
.tl-point.fruit-any { background: #8a8a8a; }

/* Organ toggle buttons in the filter panel. */
#organ-toggles {
  display: flex;
  gap: .35rem;
}
#organ-toggles button {
  min-height: 36px;
  padding: .35rem .7rem;
  font-size: .84rem;
  font-weight: 600;
  box-shadow: none;
}
#organ-toggles button[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: transparent;
}

.species-empty {
  text-align: center;
  color: var(--muted);
  font-size: .92rem;
  padding: 2rem 1rem;
}

/* Narrow screens: shrink the name column so the timeline fits. */
@media (max-width: 42rem) {
  :root { --sp-left: 5.5rem; }
  #species-content {
    padding-left: .4rem;
    padding-right: .4rem;
  }
  #species-toolbar {
    padding-left: 0;
  }
  .toolbar-right {
    padding-right: calc(2.2rem + .25rem);
  }
  .summary-row {
    gap: .35rem;
  }
  .summary-tile {
    padding: .35rem .3rem .3rem;
  }
  .summary-tile b { font-size: 1.05rem; }
  .summary-tile span { font-size: .65rem; }
  .summary-note { font-size: .66rem; }
  .month-offset {
    flex: 0 0 calc(var(--sp-left) + .4rem + 1px + 2.2rem + .25rem);
  }
  .plant-common { font-size: .78rem; }
  .plant-sci   { font-size: .7rem; }
  .plant-names.sci-only .plant-sci { font-size: .78rem; }
  .plant-names { padding-right: .35rem; }
  .plant-sep   { margin-right: .35rem; }
  .organ-label {
    flex: 0 0 2.2rem;
    padding-right: .25rem;
  }
  .organ-label svg {
    width: 1.15rem;
    height: 1.15rem;
  }
}

/* ======================================================================
   New-observation page — wizard form elements
   ====================================================================== */

/* ---------------------------------------------------------- step indicator */
#wiz-stepper { margin-bottom: 1.2rem; }
#wiz-label  { font-size: .78rem; color: var(--muted); margin: 0 0 .35rem; }
#wiz-bar    { display: flex; gap: .25rem; }
.wiz-seg {
  flex: 1; height: 4px; border-radius: 2px; background: var(--line);
  transition: background .2s;
}
.wiz-seg.done { background: var(--accent); }

/* ---------------------------------------------------------------- checkbox */
.wiz-check {
  display: flex; align-items: center; gap: .5rem;
  padding: .7rem .85rem; margin: .8rem 0;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); cursor: pointer;
}
.wiz-check input[type="checkbox"] {
  width: 1.25rem; height: 1.25rem; accent-color: var(--accent);
  flex-shrink: 0; cursor: pointer;
}
.wiz-check span { font-size: .92rem; }

/* --------------------------------------------------------- existing plants */
#existing-plant-q { margin-bottom: .4rem; }
/* "Sort by distance from me", above the picker's search box.
   Full width and 40px tall, like every other control on this page: it is
   pressed one-handed, outdoors, while holding a branch out of the way — the
   same lesson as the geolocation button in M9.11 and the language mini-picker
   above, that a control which only works at a desk fails exactly when it
   matters. Quiet styling because it is optional: the list is already in a
   useful order without it.

   "Show more" under the folded list is the same kind of control — optional,
   full width, one-handed — so it shares the rule rather than growing a second
   copy of it that drifts. */
.near-me, .load-more {
  display: block; width: 100%;
  min-height: 40px;
  margin-bottom: .4rem;
  padding: .5rem .7rem;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: var(--fg);
  font-size: .88rem; cursor: pointer;
  box-shadow: none;
}
.load-more { margin-top: .3rem; }
.near-me:hover:not(:disabled), .near-me:focus-visible,
.load-more:hover, .load-more:focus-visible { border-color: var(--accent); }
/* Disabled only while a fix is in flight, which is up to ten seconds. It has
   to look like it is working, or it gets pressed again. */
.near-me:disabled { opacity: .6; cursor: progress; }
#near-me-msg { margin: 0 0 .5rem; }

/* The row wraps the select button and the zoom button as SIBLINGS. A <button>
   inside a <button> is invalid HTML, and this is a control where a mis-tap
   silently selects the wrong plant. */
.plant-opt-row { position: relative; margin-bottom: .2rem; }

.plant-opt {
  display: flex; align-items: center; gap: .6rem;
  width: 100%; text-align: left;
  padding: .55rem .7rem;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: var(--fg);
  font-size: .88rem; cursor: pointer;
  box-shadow: none;
}
.plant-opt:hover, .plant-opt:focus-visible { border-color: var(--accent); }
.plant-opt[aria-pressed="true"] {
  border-color: var(--accent); background: var(--accent); color: var(--accent-fg);
}

/* Fixed box, so a plant with no photo occupies the same height as one with a
   picture and the list does not jump while it scrolls or filters. */
.plant-thumb {
  flex: 0 0 auto;
  width: 3.25rem; height: 3.25rem;
  border-radius: 8px; overflow: hidden;
  background: var(--bg);
  display: grid; place-items: center;
}
.plant-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.plant-thumb.is-empty { font-size: 1.3rem; opacity: .45; }

.plant-opt-text { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.plant-opt-name { font-weight: 600; line-height: 1.25; }
/* The botanical name is set apart rather than merely appended: it is a
   different KIND of name, and italic is the convention every reader of a
   species list already knows. */
.plant-opt-name em { font-weight: 400; font-style: italic; opacity: .8; }
.plant-opt-meta {
  font-size: .8rem; opacity: .72; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plant-opt[aria-pressed="true"] .plant-opt-meta,
.plant-opt[aria-pressed="true"] .plant-opt-name em { opacity: .85; }

/* Sits over the thumbnail's corner. Smaller than the 44px touch minimum on
   purpose: this is the SECONDARY action, and the tap that must not be stolen
   is selecting the plant.

   `min-height: 0` is what makes it a CIRCLE rather than a vertical ellipse.
   The global `button` rule sets `min-height: 44px` for touch, and min-height
   beats height — so `height: 1.6rem` was being ignored and the control rendered
   26px wide by 44px tall. min-width is zeroed for the same reason, before some
   later rule does the same thing on the other axis.

   White rather than var(--surface): this is a chip sitting ON a photograph, not
   a surface of the UI, so it wants to read as a badge over imagery in both
   themes. In dark mode var(--surface) is #1b1e22, which would sink into a dark
   photo and lose the affordance entirely. */
.plant-zoom {
  position: absolute; left: .35rem; bottom: .35rem;
  width: 1.6rem; height: 1.6rem;
  min-width: 0; min-height: 0;
  padding: 0;
  display: grid; place-items: center;
  font-size: .72rem; line-height: 1;
  border: 1px solid var(--line); border-radius: 50%;
  background: #fff; color: var(--fg);
  cursor: zoom-in; box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}
.plant-zoom:hover, .plant-zoom:focus-visible { border-color: var(--accent); }

/* -------------------------------------------------------------- cohort */
#cohort-block { margin-bottom: 1rem; }
#cohort-select { margin-top: .25rem; }

/* ----------------------------------------------------------- select / textarea */
select {
  width: 100%; min-height: 44px;
  padding: .55rem .8rem; font: inherit;
  color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px;
  -webkit-appearance: none; appearance: none;
}
textarea {
  width: 100%; min-height: 44px;
  padding: .6rem .8rem; font: inherit;
  color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px;
  resize: vertical;
}
textarea:focus, select:focus { border-color: var(--accent); outline: none; }

/* ------------------------------------------------------- input generic */
input[type="text"], input[type="date"], input[type="time"] {
  width: 100%; min-height: 44px;
  padding: .55rem .8rem; font: inherit;
  color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px;
}
input[type="text"]:focus,
input[type="date"]:focus,
input[type="time"]:focus { border-color: var(--accent); outline: none; }

/* Labels in the form body (not the sheet — those are different). */
#species-content label:not(.wiz-check):not(.f-legend) {
  display: block; margin: .85rem 0 .2rem;
  font-size: .82rem; font-weight: 600; color: var(--muted);
}

/* ----------------------------------------------------------- location map */
#location-map {
  height: 300px; border-radius: var(--radius);
  border: 1px solid var(--line); overflow: hidden;
}
/* Same reasoning as #map (top of this file): constructed at the hardcoded
   HOME and only moved to the visitor's own city once /api/geo answers, so it
   stays hidden until that opening view is settled — otherwise every visitor
   with no pin yet sees Singapore for a moment and then a jump. */
#location-map { opacity: 0; transition: opacity .25s ease; }
#location-map.map-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  #location-map { transition: none; }
}
#location-readout { margin-top: .4rem; }

/* ------------------------------------------------------------- photo zone */
.photo-zone {
  text-align: center; padding: 1.2rem;
  border: 2px dashed var(--line); border-radius: var(--radius);
  margin-bottom: .8rem;
}
.photo-zone button {
  font-size: 1rem; padding: .65rem 1.4rem;
}
.photo-card {
  display: flex; align-items: center; gap: .6rem;
  padding: .5rem; margin-bottom: .4rem;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface);
}
.photo-card img {
  width: 72px; height: 72px; object-fit: cover;
  border-radius: 8px; flex-shrink: 0;
}
.photo-body { flex: 1; min-width: 0; }
.photo-body select { min-height: 36px; font-size: .84rem; }
.photo-body .p-meta { font-size: .76rem; margin-top: .15rem; }
.photo-rm {
  flex-shrink: 0; min-width: 36px; min-height: 36px; padding: 0;
  border-radius: 999px; border: 1px solid var(--line);
  background: var(--surface); color: var(--muted);
  font-size: .9rem; box-shadow: none;
}

/* ---------------------------------------------------- identification cards */
#ident-results { margin: .8rem 0 1rem; }
.ident-card {
  display: block; width: 100%; text-align: left;
  /* The right padding leaves room for the ⓘ that floats on the card's corner
     (see .ident-info below) without the name running underneath it. */
  padding: .6rem 2.3rem .6rem .75rem;
  margin-bottom: .3rem;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: var(--fg);
  font-size: .88rem; cursor: pointer; box-shadow: none;
}
.ident-card:hover, .ident-card:focus-visible { border-color: var(--accent); }
.ident-card.selected {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
/* The 🔍 on each candidate — "About this species". Shaped like .plant-zoom: a
   small circle on the card's corner doing one thing, so it is a SIBLING of the
   card (a button inside a button is not valid HTML and would swallow clicks).
   `min-width: 0; min-height: 0` is what keeps it a CIRCLE: the global `button`
   rule sets `min-height: 44px` for touch, and min-height beats height — the
   same ellipse trap .plant-zoom documents above. */
.ident-card-row { position: relative; }
.ident-info {
  position: absolute; top: .5rem; right: .5rem;
  width: 1.8rem; height: 1.8rem;
  min-width: 0; min-height: 0;
  display: grid; place-items: center;
  padding: 0; margin: 0;
  border: 1px solid var(--line); border-radius: 50%;
  background: var(--surface); color: var(--muted);
  font-size: .95rem; line-height: 1;
  cursor: pointer; box-shadow: none;
}
.ident-info:hover, .ident-info:focus-visible { border-color: var(--accent); color: var(--accent); }
.ident-score-bar {
  height: 5px; background: var(--bg); border-radius: 3px;
  overflow: hidden; margin: .25rem 0 0;
}
.ident-score-fill {
  height: 100%; background: var(--accent); border-radius: 3px;
  transition: width .3s;
}

/* -------------------------------------------------------- duplicate warning */
.dup-warn {
  padding: .6rem .8rem; margin: .6rem 0;
  border: 1px solid var(--warm); border-radius: 10px;
  background: color-mix(in srgb, var(--warm) 8%, var(--surface));
  color: var(--fg); font-size: .86rem;
}
.dup-warn p { margin: 0 0 .4rem; }
.dup-warn button {
  padding: .35rem .7rem; font-size: .82rem; box-shadow: none;
}

/* --------------------------------------------------------- phenophase grid */
.pp-category { margin-bottom: 1.4rem; }
.pp-cat-head {
  display: flex; align-items: center; gap: .6rem;
  margin-bottom: .35rem;
}
.pp-cat-head h3 { margin: 0; font-size: 1.05rem; flex: 1; }
.pp-cat-head .p-meta { font-size: .78rem; }
.pp-cat-head button {
  padding: .3rem .65rem; font-size: .78rem; box-shadow: none;
}
.pp-row {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .45rem .25rem; border-top: 1px solid var(--line);
}
.pp-label { flex: 1; font-size: .88rem; min-width: 8rem; }

/* Toggle buttons for yes / no / unsure */
.pp-toggle { display: flex; gap: 0; flex-shrink: 0; }
.pp-toggle button {
  min-height: 34px; min-width: 3rem;
  padding: .3rem .55rem; font-size: .8rem; font-weight: 600;
  border: 1px solid var(--line); border-radius: 0; box-shadow: none;
  background: var(--surface); color: var(--fg);
}
.pp-toggle button:first-of-type { border-radius: 999px 0 0 999px; }
.pp-toggle button:last-of-type  { border-radius: 0 999px 999px 0; }
.pp-toggle button[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-fg);
  border-color: transparent;
}
.pp-toggle button[aria-pressed="true"][data-status="unsure"] {
  background: none; color: var(--warm);
  border-color: var(--warm) !important;
}

/* ------------------------------------------------------ intensity slider */
.pp-intensity {
  flex-basis: 100%; margin-top: .2rem;
  display: flex; align-items: center; gap: .5rem;
}
.pp-intensity input[type="range"] {
  flex: 1; height: 36px;
  accent-color: var(--accent);
}
.pp-int-label { font-size: .78rem; min-width: 4.5rem; }

/* -------------------------------------------------------- wizard buttons */
.wiz-nav {
  display: flex; gap: .5rem; margin-top: 1.3rem;
}
.wiz-nav button { flex: 1; }
.wiz-nav button:only-child { flex: 0 0 auto; min-width: 12rem; }

/* Two-column row */
.wiz-row { display: flex; gap: .7rem; }
.wiz-col { flex: 1; min-width: 0; }

/* --------------------------------------------------------- done / confirm */
#wiz-done {
  max-width: 32rem; margin: 5rem auto 0;
  padding: 0 1rem;
}
.done-card {
  padding: 1.5rem;
  border-radius: var(--radius);
  background: var(--surface); box-shadow: var(--shadow);
}
.done-card h2 { margin: 0 0 .5rem; font-size: 1.15rem; }
.done-card .p-meta { margin-bottom: 1rem; }
.done-card button { width: 100%; }
.done-card .primary { margin-bottom: 0; }

/* Bottom-left cross-links on non-map pages. Same fixed placement as the
   map page's #bottom-left so the navigation reads as one consistent bar
   across every page. #species-content carries extra padding-bottom so the
   page body scrolls clear of the buttons rather than sliding under them. */
#species-map-btn-wrap {
  position: fixed; z-index: 4;
  left: 1rem; bottom: var(--nav-gap);
  display: flex; align-items: center; gap: .38rem;
}

/* --------------------------------------------------------- review card */
#review-card {
  padding: .8rem 1rem; margin-bottom: 1rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
}
#review-card h3 { margin: 0 0 .3rem; font-size: 1.1rem; }
#review-card .pill {
  font-size: .7rem; padding: .08rem .4rem;
  vertical-align: middle; margin-left: .3rem;
}

/* ------------------------------------------------------------- reminders */
/* The sheet chrome itself is .sheet, shared with lib/export.js and
   lib/sharing.js. Only the few bits unique to this panel live here. */

/* --------------------------------------------------------- resume a draft */
/* Same measure and placement as #wiz-done: it occupies the same moment in the
   flow — the screen you meet instead of a wizard step — so it should not
   arrive at a different width or height on the page. */
#draft-resume {
  max-width: 32rem; margin: 3rem auto 0;
  padding: 1.5rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); box-shadow: var(--shadow);
}
#draft-resume h2 { margin: 0 0 .4rem; font-size: 1.15rem; }
#draft-resume p { margin: 0 0 .9rem; font-size: .92rem; }
.draft-resume-actions { display: flex; flex-direction: column; gap: .5rem; }
/* Stacked, not side by side, and the discarding one second. On a phone these
   are two full-width targets a thumb reaches without aiming, and the
   destructive answer should not sit where the confirming one usually does. */
.draft-resume-actions button { width: 100%; }

/* --------------------------------------------------------- section spacing */
#species-content section[data-step] h2 {
  margin: 0 0 .6rem; font-size: 1.3rem;
}
#species-content section[data-step] h3 {
  margin: 1.2rem 0 .4rem; font-size: 1.05rem;
}

/* Phone adjustments for the wizard */
@media (max-width: 42rem) {
  .wiz-row { flex-direction: column; gap: 0; }
  .pp-row { flex-direction: column; align-items: flex-start; gap: .3rem; }
  .pp-label { font-size: .84rem; }
  .pp-toggle button { min-width: 2.6rem; font-size: .76rem; }
  #location-map { height: 250px; }
}

/* ======================================================================
   All-records page
   ====================================================================== */

/* Record list row — full-width button card */
.rec-row {
  display: flex; gap: .7rem; align-items: flex-start;
  width: 100%; text-align: left;
  padding: .6rem .7rem; margin-bottom: .3rem;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: var(--fg);
  font: inherit; cursor: pointer; box-shadow: none;
}
.rec-row:hover, .rec-row:focus-visible { border-color: var(--accent); }
.rec-thumb {
  flex-shrink: 0; width: 56px; height: 56px;
  border-radius: 8px; overflow: hidden;
  background: var(--bg);
}
.rec-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.rec-body { flex: 1; min-width: 0; }
.rec-body strong { display: block; font-size: .95rem; margin-bottom: .1rem; }
.rec-meta { display: block; font-size: .78rem; color: var(--muted); }

/* Edit panel inner form */
.edit-form label {
  display: block; margin: .75rem 0 .15rem;
  font-size: .82rem; font-weight: 600; color: var(--muted);
}
.edit-form input[type="text"],
.edit-form textarea {
  width: 100%; min-height: 44px;
  padding: .55rem .8rem; font: inherit;
  color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px;
}
.edit-form textarea { resize: vertical; }
.edit-form input[type="date"],
.edit-form input[type="time"] {
  width: 100%; min-height: 44px;
  padding: .5rem .8rem; font: inherit;
  color: var(--fg); background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px;
}
.edit-form h3 { margin: 1rem 0 .3rem; font-size: .95rem; }
.edit-form .p-meta { font-size: .76rem; margin: .15rem 0; }

/* Location map inside edit panel */
.edit-loc-map {
  height: 240px; border-radius: var(--radius);
  border: 1px solid var(--line); overflow: hidden;
  margin-top: .3rem;
}

/* Cut flower / indoor, under the location map. `.wiz-check` carries the box and
   the tap target — same control as the capture wizard, so the two surfaces do
   not disagree about what these flags look like. */
.edit-flags { margin: .5rem 0 .2rem; }
.edit-flags .wiz-check { margin: .4rem 0; }

/* Photo grid inside edit panel */
.photo-grid { margin: .4rem 0; }

@media (max-width: 42rem) {
  .rec-thumb { width: 44px; height: 44px; }
  .rec-body strong { font-size: .88rem; }
  .edit-loc-map { height: 200px; }
}

/* ------------------------------------------------------------ export sheet */
/* Built by lib/export.js, styled here. It borrows `.sheet` for the backdrop and
   the card, and the filter panel's own controls (.f-legend, .f-dates, .f-chip,
   .f-person, .f-empty) for the pickers — the People filter on All Records is
   the control this dialog is imitating, and imitating it in a second set of
   class names is how the two start to look like different products.

   `position: fixed`, unlike `.sheet`'s `absolute`: this dialog opens from the
   account menu on pages that scroll (All Records, My Species), and an absolute
   overlay anchors to the document, so on a page scrolled halfway down it would
   open above the viewport. */
#export-sheet { position: fixed; }

/* Same reasoning as #export-sheet above: the observation edit overlay (opened
   from the pencil icon on the map and My Species detail panels) reuses .sheet
   too, and My Species scrolls. Without this, editing a plant near the bottom
   of a scrolled page opens the sheet above the current viewport. */
#edit-panel { position: fixed; }

/* ---------------------------------------------------------- species info */
/* Built by lib/species-info.js — the ⓘ on an identification candidate. It
   borrows `.sheet` for the backdrop and card like #edit-panel does, and for
   the same reason it must be `fixed`: the wizard page scrolls, and an absolute
   overlay would open above the viewport. Everything shown here is fetched
   live from GBIF's public API and is never stored anywhere (see the module's
   header comment). */
#species-sheet { position: fixed; }
.species-card { position: relative; width: min(36rem, 100%); }
.species-close {
  position: absolute; top: .6rem; right: .6rem;
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  padding: 0; margin: 0;
  border: none; border-radius: 50%;
  background: transparent; color: var(--muted);
  font-size: 1rem; cursor: pointer;
}
.species-close:hover, .species-close:focus-visible {
  color: var(--fg); background: var(--bg);
}
.species-name {
  margin: 0 2.2rem .2rem 0;   /* clears the ✕ in the corner */
  font-size: 1.25rem; font-style: italic; font-weight: 600;
}
.species-authors { font-style: normal; font-weight: 400; color: var(--muted); font-size: .9rem; }
.species-taxa {
  display: grid; grid-template-columns: max-content 1fr;
  gap: .2rem .8rem; margin: .8rem 0 0;
  font-size: .9rem;
}
.species-taxa dt { color: var(--muted); }
.species-taxa dd { margin: 0; }
.species-common { margin: .8rem 0 0; font-size: .88rem; }
.species-photos { margin-top: .8rem; }
.species-photo-track {
  display: flex; overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;               /* the arrows and swipe are the controls */
}
.species-photo-track::-webkit-scrollbar { display: none; }
.species-photo {
  flex: 0 0 100%; margin: 0; scroll-snap-align: start;
  border-radius: 10px; overflow: hidden; background: var(--bg);
}
.species-photo img { display: block; width: 100%; max-height: 14rem; object-fit: cover; }
.species-photo-nav {
  display: flex; align-items: center; justify-content: center;
  gap: .8rem; margin-top: .4rem;
}
.species-photo-nav button {
  width: 2rem; height: 2rem;
  min-width: 0; min-height: 0;   /* circles, not ellipses — see .plant-zoom */
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 50%;
  background: var(--surface); color: var(--fg);
  font-size: 1.1rem; cursor: pointer; box-shadow: none;
}
.species-photo-nav button:hover, .species-photo-nav button:focus-visible {
  border-color: var(--accent);
}
.species-photo-count { color: var(--muted); font-size: .85rem; min-width: 3rem; text-align: center; }
.species-credit { margin: .3rem 0 0; color: var(--muted); font-size: .78rem; }
.species-open {
  display: inline-block; margin-top: 1rem;
  color: var(--accent); font-weight: 600; text-decoration: none;
}
.species-open:hover, .species-open:focus-visible { text-decoration: underline; }

.ex-lede {
  margin: 0 0 1rem;
  font-size: .86rem;
  line-height: 1.5;
  color: var(--muted);
}
.ex-note { font-size: .86rem; margin: .8rem 0 0; color: var(--muted); }

/* ------------------------------------------------------------ data sharing */
/* The status line is the one sentence somebody opens this dialog to read, so it
   is full-strength text rather than muted like the explanations below it. */
.sh-status {
  margin: .2rem 0 .9rem;
  font-size: .95rem;
  line-height: 1.45;
  color: var(--fg);
}
.sh-notes p {
  margin: .75rem 0 0;
  font-size: .84rem;
  line-height: 1.5;
  color: var(--muted);
}
.sh-notes { margin-top: .9rem; }
.sh-note { font-size: .86rem; margin: .8rem 0 0; color: var(--muted); }

/* The two policy documents. Quiet — they are a reference, not an action — but
   underlined and padded to a real tap target, because the lesson from
   #copyright is that a policy link nobody can hit is a policy link nobody
   reads. */
.sh-policies {
  margin: .9rem 0 0;
  font-size: .84rem;
  line-height: 1.5;
  color: var(--muted);
}
.sh-policies a {
  display: inline-block;
  padding: .25rem .1rem;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: .18em;
}
.sh-policies a:hover, .sh-policies a:focus-visible { color: var(--fg); }

/* The explanation is four paragraphs, so on a short viewport the card outgrows
   the screen and Save/Cancel land below the fold — and the sheet closes on a
   backdrop click, so a tap aimed at a button that is not there discards the
   change silently. Caught by clicking Save in a 569px-tall window and watching
   the dialog close without saving.
   Same fix and same reasoning as #export-form: the form is the column, the
   NOTES are the part that gives, and the action row is always on screen. */
#share-form {
  display: flex;
  flex-direction: column;
  max-height: 100%;
}
#share-form > * { flex: 0 0 auto; }
#share-form .sh-notes {
  flex: 1 1 auto;
  min-height: 0;          /* or a flex child refuses to shrink below its content */
  overflow-y: auto;
}

/* `.wiz-check` keeps its checkbox from stretching, because
   `.wiz-check input[type="checkbox"]` out-specifies `.sheet input`. Its OWN
   layout does not survive the trip, though: `.sheet label` is (0,1,1) and
   `.wiz-check` is (0,1,0), so `display:block` wins and the checkbox drops onto
   a line of its own — and the row inherits the muted 600-weight styling meant
   for field captions above an input, which this is not. Restated here at a
   specificity that wins inside a sheet, for this dialog and any later one. */
.sheet .wiz-check {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .55rem 0;
  font-size: inherit;
  font-weight: 400;
  color: var(--fg);
}
.sheet .wiz-check span { line-height: 1.4; }
/* A disabled control must look disabled — the box is switched off for somebody
   with no task, or whose task has peer sharing suspended. */
#share-form .wiz-check:has(input:disabled) { opacity: .55; cursor: not-allowed; }

/* `.sheet label` would otherwise win on source order and give each date label a
   .9rem top margin, breaking the two-column grid it sits in. */
#export-form .f-dates label { margin: 0; font-weight: 400; }
#export-form .f-group { margin-bottom: 1rem; }

#ex-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
#ex-chips:not(:empty) { margin-bottom: .4rem; }

/* The card is a column in which only the roster gives, so the buttons are
   always the last thing on screen and nothing scrolls underneath them.
   #filters solves the same problem the same way.

   Found by opening this dialog as a LEAD on a laptop: the roster adds a whole
   section, the card outgrows the viewport, and both buttons land below its
   fold. A member never sees it, because a member has no Observers section — so
   the role that gets the larger dialog was the role that could not reach the
   button that runs it. Making the row `position: sticky` fixed the reach and
   left the list scrolling visibly *under* the buttons, which reads as a
   rendering bug; capping the card and letting the list absorb the remainder is
   the version that also looks right. */
#export-form {
  display: flex;
  flex-direction: column;
  max-height: 100%;
}
#export-form > * { flex: 0 0 auto; }
#ex-people-group {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 6rem;
}
#ex-person-list {
  margin-top: .35rem;
  flex: 1 1 auto;
  min-height: 0;          /* or a flex child refuses to shrink below its content */
  overflow-y: auto;
}

/* ================================================================ the account
   pages: /register, /onboarding, /auth-callback, /reset-password.

   The pages in this app that are NOT a map with things floating over it.
   `body.page-form` opts out of the fixed, full-viewport layout the map
   needs — these scroll, because a consent text that cannot be scrolled to is a
   consent that was not read.

   They reuse `.sheet`'s form furniture (label, input, .row) deliberately: the
   sign-in dialog and the registration form are the same kind of object and
   should not diverge in padding and border radius for no reason. What they do
   NOT reuse is `.sheet` itself, which is a modal backdrop pinned to `inset: 0`
   and would trap the page at viewport height. */
body.page-form {
  position: static;
  overflow-y: auto;
  min-height: 100%;
  background: var(--bg);
}
body.page-form #chrome { position: static; }

#reg, #ob, #cb, #rp {
  width: min(34rem, 100%);
  margin: 0 auto;
  padding: 1.2rem 1.1rem 4rem;
}
#reg h1, #ob h1, #cb h1, #rp h1 { margin: .6rem 0 .4rem; font-size: 1.35rem; }
#reg .lede, #ob .lede, #rp .lede { margin: 0 0 1.4rem; color: var(--muted); line-height: 1.5; }

/* Same furniture as the sign-in sheet, applied to plain forms on a page. */
#reg-form label, #ob-form label, #rp-form label {
  display: block; margin: .9rem 0 .25rem;
  font-size: .82rem; font-weight: 600; color: var(--muted);
}
#reg-form input[type="text"], #reg-form input[type="email"],
#reg-form input[type="password"], #ob-form input[type="text"],
#rp-form input[type="password"] {
  width: 100%; padding: .7rem .8rem; min-height: 44px;
  font: inherit; color: var(--fg); background: var(--surface);
  border: 1px solid var(--line); border-radius: 10px;
}
#reg .row, #ob .row, #rp .row { display: flex; gap: .5rem; margin-top: 1.2rem; }
#reg .row > *, #ob .row > *, #rp .row > * { flex: 1; }

/* The hint under the credit-name field carries the fact that the name is
   public — and, since migration 054, changeable later from the account menu.
   It is the one piece of small print on this page that must not read as small
   print, so it is the same size as the body text and only its colour is
   quieter. */
#reg .hint, #ob .hint, #rp .hint {
  margin: .3rem 0 0; font-size: .84rem; line-height: 1.5; color: var(--muted);
}
#reg .hint.ok, #ob .hint.ok, #rp .hint.ok { color: var(--ok, var(--muted)); }
#reg .hint.warn, #ob .hint.warn { color: var(--danger); }

/* Third-party buttons: full width, stacked, above the form. Neutral rather than
   carrying each provider's brand colour — this project ships no third-party
   logos, and a coloured button with no logo reads as an imitation of one. */
#reg-provider-buttons { display: flex; flex-direction: column; gap: .5rem; }
#reg-provider-buttons .provider {
  width: 100%; min-height: 46px; font: inherit; font-weight: 600;
  color: var(--fg); background: var(--surface);
  border: 1px solid var(--line); border-radius: 10px; cursor: pointer;
}
#reg-provider-buttons .provider:hover { border-color: var(--fg); }

/* The divider between "continue with a provider" and the form. */
#reg .or {
  display: flex; align-items: center; gap: .8rem;
  margin: 1.3rem 0 .2rem; color: var(--muted); font-size: .82rem;
}
#reg .or::before, #reg .or::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}

/* The consents. Each is a whole sentence with a link in it, so the label wraps
   beside the box rather than under it — an unticked box that has drifted away
   from its own sentence is how somebody agrees to the wrong thing. */
#reg-consents, #ob-consents {
  margin: 1.4rem 0 0; padding: .9rem 1rem; border: 1px solid var(--line);
  border-radius: 10px; background: var(--surface);
}
#reg-consents legend, #ob-consents legend {
  padding: 0 .4rem; font-size: .82rem; font-weight: 600; color: var(--muted);
}
#reg-consents .check, #ob-consents .check {
  display: flex; gap: .6rem; align-items: flex-start;
  margin: .7rem 0; font-size: .9rem; line-height: 1.5;
  font-weight: 400; color: var(--fg);
}
#reg-consents .check input, #ob-consents .check input {
  flex: 0 0 auto; width: 20px; height: 20px; margin-top: .12rem;
}

/* A link that has to sit in a button row beside a real button. */
.button-ish {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 0 1rem; border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--fg); text-decoration: none; font-weight: 600;
}

/* The optional sharing box. Visibly a different object from the three consents
   above it — quieter border, no fill — because §2A.4's rule is that a mandatory
   consent and an optional preference must not look like items in one list.
   Somebody scanning the form should be able to tell, without reading, that this
   is the one they may decline. */
#reg-sharing, #ob-sharing {
  margin: 1rem 0 0; padding: .9rem 1rem;
  border: 1px dashed var(--line); border-radius: 10px; background: none;
}
#reg-sharing legend, #ob-sharing legend {
  padding: 0 .4rem; font-size: .82rem; font-weight: 600; color: var(--muted);
}
#reg-sharing .check, #ob-sharing .check {
  display: flex; gap: .6rem; align-items: flex-start;
  margin: .6rem 0 .2rem; font-size: .9rem; line-height: 1.5; color: var(--fg);
}
#reg-sharing .check input, #ob-sharing .check input {
  flex: 0 0 auto; width: 20px; height: 20px; margin-top: .12rem;
}
#reg-sharing .hint, #ob-sharing .hint { margin-top: .5rem; }

/* ----------------------------------------------------- /privacy and /terms

   Two documents rather than app screens, and they carry no JavaScript at all —
   a visitor must be able to read them before they have an account, and Google's
   OAuth consent screen fetches the privacy URL directly. So everything here is
   plain CSS over plain markup, with all three languages stacked.

   Wider measure than the forms and a taller line, because these are read in
   paragraphs rather than scanned in fields. */
#policy {
  width: min(46rem, 100%);
  margin: 0 auto;
  padding: 1.2rem 1.1rem 4rem;
  line-height: 1.65;
}
#policy h1 { margin: 1.4rem 0 .3rem; font-size: 1.5rem; }
#policy h2 {
  margin: 2rem 0 .5rem;
  font-size: 1.05rem;
  padding-top: .9rem;
  border-top: 1px solid var(--line);
}
#policy p, #policy li { font-size: .95rem; }
#policy ul { margin: .5rem 0 .5rem 1.1rem; padding: 0; }
#policy li { margin: .4rem 0; }
#policy .policy-meta { color: var(--muted); font-size: .85rem; margin: 0 0 1.4rem; }

/* The language jump-links. A stacked document is only usable if the reader can
   get to their own language in one tap from the top. */
.policy-langs {
  position: sticky;
  top: 0;
  z-index: 5;
  margin: 0 -1.1rem;
  padding: .7rem 1.1rem;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  font-size: .88rem;
  color: var(--muted);
}

/* Each language starts a page's worth down, so a jump link lands on a heading
   with nothing of the previous language still on screen to confuse it. */
#policy section { scroll-margin-top: 3.2rem; padding-top: 1.5rem; }
#policy section + section { border-top: 3px double var(--line); margin-top: 3rem; }

.policy-foot {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
  font-size: .88rem;
  color: var(--muted);
}

/* --------------------------------------------- the signed-out sign-in panel

   Everything you can do while signed out, in one box hung off the button that
   opened it. It replaced two things: a full-screen `.sheet` modal that dimmed
   the map for two fields, and a drop-down of methods that asked people to
   choose before they could see what they were choosing between.

   `#signin-wrap` exists only to give the panel something to be positioned
   against. Without it the panel anchors to #signedout-actions and slides under
   the language button. */
#signin-wrap { position: relative; }

#signin-panel {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: .4rem;
  /* Wide enough for an email address, and never wider than the screen. The
     viewport clamp is what keeps it usable on a phone, where `right: 0` on a
     fixed-width box would otherwise push its left edge off the display. */
  width: 20rem;
  max-width: calc(100vw - 1.6rem);
  padding: .9rem 1rem 1rem;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
  z-index: 30;
  /* A long panel on a short phone in landscape scrolls rather than spilling
     behind the map controls. */
  max-height: calc(100vh - 5rem);
  overflow-y: auto;
}

/* Labels and fields left-aligned even though the account menu right-aligns its
   items: a form is read left to right regardless of which edge it hangs from,
   and a right-aligned label above a full-width input reads as a caption. */
#signin-panel label {
  display: block;
  margin: .7rem 0 .25rem;
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
}
#signin-panel label:first-child { margin-top: 0; }
#signin-panel input {
  width: 100%;
  padding: .65rem .75rem;
  min-height: 44px;
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
}
#signin-panel .row { display: flex; gap: .5rem; margin-top: .9rem; }
#signin-panel .row button { flex: 1; box-shadow: none; min-height: 42px; }

/* The divider. Same shape as the one on /register so the two entrances to this
   app do not look like two different products. */
#signin-panel .or {
  display: flex;
  align-items: center;
  gap: .7rem;
  margin: 1rem 0 .8rem;
  color: var(--muted);
  font-size: .8rem;
}
#signin-panel .or::before, #signin-panel .or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* The heading the provider buttons finish: "Sign in with" · GitHub · Google.
   Styled as a caption rather than as a heading, because it is the same kind of
   thing as the field labels above it — a word that says what the control under
   it is for. */
#signin-panel .signin-with {
  margin: 0 0 .45rem;
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
}

/* Provider buttons: ONE ROW, equal width, neutral. No brand colours and no
   logos — this project ships no third-party marks, and a coloured button
   without its logo reads as an imitation of one.

   Stacked full-width until 2026-08-10. A row is right once they are bare
   product names under a "Sign in with" heading: two 44px bars saying "GitHub"
   and "Google" one above the other took a third of the panel to say what one
   line says, and pushed the sign-up button below the fold on a short phone.
   `flex: 1 1 0` with `min-width: 0` so a third provider divides the row rather
   than overflowing it. */
#signin-providers { display: flex; flex-direction: row; gap: .45rem; }
#signin-providers button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  font: inherit;
  font-weight: 600;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: none;
  cursor: pointer;
}
#signin-providers button:hover { border-color: var(--fg); }
#signin-providers button[hidden] { display: none; }

/* "Sign up with email" — full width, and deliberately NOT `.primary`. The
   primary action in this panel is signing in; a filled button here would
   compete with it and would be the loudest control on the screen for the
   visitor who least needs it (the returning member). */
#signin-signup {
  width: 100%;
  min-height: 44px;
  font: inherit;
  font-weight: 600;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: none;
  cursor: pointer;
}
#signin-signup:hover { border-color: var(--fg); }
#signin-signup[hidden] { display: none; }

/* "Forgot your password?" — a button that has to read as a link, because what
   it does is not "go somewhere" but it is not a form action either. Padded to a
   real tap target: it is the control somebody reaches for on a phone, at the
   moment they are already frustrated. */
.linky {
  /* inline-FLEX rather than inline-block, and `min-height` restated rather
     than inherited: the base `button` rule sets 44px, and an inline-block with
     a min-height taller than its text leaves the text stuck to the top of an
     invisible box. 40px matches `.lang-mini`, the other secondary control in
     this chrome, and is still a target a thumb can find. */
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  margin: .3rem 0 0;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  font: inherit;
  font-size: .82rem;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: .18em;
  cursor: pointer;
}
.linky:hover, .linky:focus-visible { color: var(--fg); }
.linky:disabled { opacity: .55; cursor: default; }

/* The panel's message line when the message is NOT a failure — "we have sent
   you a link". Same size as the error it shares an element with, without the
   colour that would make good news look like bad. */
#signin-panel .auth-note {
  margin: .6rem 0 0;
  font-size: .84rem;
  line-height: 1.5;
  color: var(--muted);
}

/* The line that tells a first-time visitor this will also create an account. */
#signin-panel .menu-hint {
  margin: .8rem 0 0;
  font-size: .78rem;
  line-height: 1.45;
  color: var(--muted);
}

/* ================================================================= messages */
/* The inbox (/messages). Reuses the species-page frame (scrollable body, the
   bottom nav band) and layers its own header + card list inside. */
.msg-page #msg-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin-bottom: 1rem;
}
.msg-page #msg-header h1 {
  margin: 0;
  font-size: 1.35rem;
  line-height: 1.15;
}
.msg-page #msg-mark-all {
  font-size: .82rem;
  padding: .4rem .7rem;
  min-height: 40px;
}

/* The "Receive notifications" switch. Same visual language as .wiz-check: a
   tappable row whose accent shows only in the checkbox itself. */
.msg-notif {
  display: flex; align-items: center; gap: .5rem;
  padding: .7rem .85rem; margin: 0 0 .8rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); cursor: pointer;
}
.msg-notif input[type="checkbox"] {
  width: 1.25rem; height: 1.25rem; accent-color: var(--accent);
  flex-shrink: 0; cursor: pointer;
}
.msg-notif span { font-size: .92rem; }
.msg-notif input:disabled + span { opacity: .6; }

/* The timezone picker — a slim inline row, not a card, so it reads as a quiet
   control rather than a block. */
.msg-tz {
  display: flex;
  align-items: center;
  gap: .45rem;
  margin: 0 0 .4rem;
}
.msg-tz span {
  font-size: .88rem;
  color: var(--muted);
  flex-shrink: 0;
}
.msg-tz select {
  width: auto;
  min-height: 34px;
  font-size: .88rem;
  padding: .2rem .4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
}
.msg-tz select:focus { outline: none; }

/* The delivery note at the bottom of the messages page — small, grey,
   left-aligned, always visible. */
.msg-info-note {
  margin: 1.2rem 0 0;
  font-size: .8rem;
  line-height: 1.5;
  color: var(--muted);
}

/* ----------------------------------------------------------------- settings */
/* The consolidated account page. Sections read as the same bordered cards as
   the messages list — one surface per concern. */
.settings-page #settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin-bottom: 1rem;
}
.settings-page #settings-header h1 {
  margin: 0;
  font-size: 1.35rem;
  line-height: 1.15;
}
.settings-sec {
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  margin-bottom: 1rem;
}
.settings-sec > h2 {
  margin: 0 0 .6rem;
  font-size: 1.05rem;
  line-height: 1.2;
}
.pk-logo {
  display: block;
  margin: 0 0 .75rem;
}
.pk-logo img {
  display: block;
  max-width: 100%;
  height: auto;
}
.settings-sec .row {
  display: flex;
  gap: .5rem;
  margin-top: 1.1rem;
}

#msg-list {
  display: flex;
  flex-direction: column;
  gap: .65rem;
}

/* One card. The unread state is a red left edge, not a tinted background —
   colour is spent sparingly on this map-first UI (see the palette note at the
   top of this file), and the edge travels with the text if the row wraps. */
.msg-card {
  padding: .85rem 1rem;
  border: 1px solid var(--line);
  border-left: 4px solid transparent;
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.msg-card.unread { border-left-color: var(--danger); }
.msg-card.clickable { cursor: pointer; }
.msg-card.clickable:hover, .msg-card.clickable:focus-visible {
  border-color: var(--accent);
}
.msg-card-head {
  display: flex;
  align-items: baseline;
  gap: .6rem;
}
.msg-card-title { flex: 1; font-size: .98rem; }
.msg-card-when {
  flex-shrink: 0;
  font-size: .74rem;
  color: var(--muted);
}
.msg-chip {
  flex-shrink: 0;
  font-size: .68rem;
  font-weight: 600;
  padding: .12rem .45rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}
.msg-chip.new {
  border-color: var(--danger);
  color: var(--danger);
}
.msg-card-body {
  margin: .45rem 0 0;
  font-size: .9rem;
  line-height: 1.5;
  color: var(--fg);
  white-space: pre-wrap;      /* the sender's line breaks survive */
}

/* Response buttons on inbox cards (migration 049). */
.msg-card-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: .65rem;
}
.msg-resp-btn {
  font-size: .82rem;
  padding: .4rem .75rem;
  min-height: 38px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--accent);
  cursor: pointer;
}
.msg-resp-btn:hover, .msg-resp-btn:focus-visible {
  background: var(--accent);
  color: var(--accent-fg);
}
.msg-card-response {
  margin: .45rem 0 0;
  font-size: .82rem;
  color: var(--muted);
  font-style: italic;
}

/* Subtitle line on inbox cards (migration 051) — a secondary line under the
   head, reading as support for the title rather than as the body. */
.msg-card-subtitle {
  margin: .3rem 0 0;
  font-size: .84rem;
  font-weight: 600;
  color: var(--muted);
}

/* Collapsed long body. The max-height (3 lines at the body's 1.5 line-height
   and .9rem size) is the truncation; messages.js toggles the class. */
.msg-card-body.collapsed {
  max-height: 4.05em;        /* 3 × 1.5 × .9rem */
  overflow: hidden;
  transition: max-height .2s ease;
}

/* "Show more / Show less" — the card is the toggle; this is the affordance. */
.msg-card-toggle {
  margin: .35rem 0 0;
  padding: 0;
  border: none;
  background: none;
  font-size: .82rem;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}
.msg-card-toggle:hover, .msg-card-toggle:focus-visible {
  text-decoration: underline;
}

/* Free-text reply (migration 052). Shown on unanswered cards — always for short
   ones, on long ones only when expanded. Kept in the DOM (hidden) so a draft
   survives collapse. */
.msg-card-reply {
  display: flex;
  flex-direction: column;
  gap: .45rem;
  margin-top: .65rem;
  padding-top: .65rem;
  border-top: 1px solid var(--line);
}
.msg-card-reply[hidden] { display: none; }
.msg-card-reply-input {
  width: 100%;
  padding: .5rem .65rem;
  font-size: .88rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
  resize: vertical;
  min-height: 56px;
}
.msg-card-reply-err {
  margin: 0;
  font-size: .8rem;
  color: var(--danger);
}
.msg-reply-send {
  align-self: flex-end;
  font-size: .82rem;
  padding: .4rem .8rem;
  min-height: 38px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--accent);
  cursor: pointer;
}
.msg-reply-send:hover, .msg-reply-send:focus-visible {
  background: var(--accent);
  color: var(--accent-fg);
}
.msg-reply-send:disabled {
  opacity: .6;
  cursor: default;
}

/* Deep-link highlight (opened from a notification ?msg=id) — a brief accent
   ring so the card that was the notification is the one the eye lands on. */
.msg-card-highlight {
  box-shadow: 0 0 0 2px var(--accent);
  animation: msg-highlight-fade 2.6s ease forwards;
}
@keyframes msg-highlight-fade {
  0%, 60% { box-shadow: 0 0 0 2px var(--accent); }
  100%    { box-shadow: var(--shadow); }
}

/* ================================================================= send-message */
/* The dedicated /send-message page. Two sections stacked: compose form on top,
   sent-history list below. Follows the same species-page frame. */
.send-page .send-section {
  margin-bottom: 1.8rem;
}
.send-page .send-section h2 {
  margin: 0 0 .8rem;
  font-size: 1.2rem;
  line-height: 1.2;
}

/* The compose form inside the send page. Same fields as the old sheet, plus
   optional response-button text inputs. */
.send-page #send-form {
  display: flex;
  flex-direction: column;
  gap: .3rem;
}
.send-page #send-form label {
  margin-top: .5rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--muted);
}
.send-page #send-form select,
.send-page #send-form input[type="text"],
.send-page #send-form textarea {
  width: 100%;
  padding: .55rem .7rem;
  font-size: .92rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
}
.send-page #send-form textarea {
  resize: vertical;
  min-height: 80px;
}

/* The optional response-button inputs. */
.send-buttons-field {
  margin-top: .8rem;
  padding: .7rem .85rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.send-buttons-field legend {
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
  padding: 0 .3rem;
}
.send-btn-row {
  display: flex;
  flex-direction: column;
  gap: .4rem;
}
.send-btn-row input {
  width: 100%;
  padding: .5rem .65rem;
  font-size: .88rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
}

/* Text-reply opt-in checkbox (data.allow_reply) — sits under the buttons
   fieldset, same field-label rhythm as the rest of the compose form. */
.send-reply-check {
  display: flex;
  align-items: center;
  gap: .45rem;
  margin-top: .7rem;
  font-size: .88rem;
  cursor: pointer;
}
.send-reply-check input {
  width: 1rem;
  height: 1rem;
  accent-color: var(--accent);
}

/* -- sent-history cards -- */
#sent-list {
  display: flex;
  flex-direction: column;
  gap: .65rem;
}

.sent-card {
  padding: .85rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.sent-card-head {
  display: flex;
  align-items: baseline;
  gap: .6rem;
}
.sent-card-title { flex: 1; font-size: .98rem; }
.sent-card-when {
  flex-shrink: 0;
  font-size: .74rem;
  color: var(--muted);
}
.sent-card-recipient {
  margin-top: .25rem;
  font-size: .78rem;
  color: var(--muted);
}
.sent-card-body {
  margin: .45rem 0 0;
  font-size: .9rem;
  line-height: 1.5;
  color: var(--fg);
  white-space: pre-wrap;
}
.sent-card-status {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin-top: .55rem;
  flex-wrap: wrap;
}
.sent-chip {
  flex-shrink: 0;
  font-size: .68rem;
  font-weight: 600;
  padding: .12rem .45rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}
.sent-chip.new {
  border-color: var(--danger);
  color: var(--danger);
}
.sent-card-response {
  font-size: .78rem;
  color: var(--accent);
}
.sent-card-offered {
  font-size: .76rem;
  color: var(--muted);
}
.sent-card-subtitle {
  margin: .25rem 0 0;
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
}

/* The "Load 15 more" button wrapper. */
.load-more-wrap {
  text-align: center;
  margin-top: .8rem;
}
.load-more-wrap .btn-link {
  font-size: .88rem;
  min-height: 42px;
}

/* ================================================================= templates */
/* The admin template library page (/templates, migration 058): a card list on
   top, the create/edit form below. Mirrors the send-page visual language —
   same fields, same rhythm — so the two forms read as one surface. */
.templates-page .send-section {
  margin-bottom: 1.8rem;
}
.templates-page .send-section h2 {
  margin: 0 0 .8rem;
  font-size: 1.2rem;
  line-height: 1.2;
}

.tpl-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  margin-bottom: .8rem;
}
.templates-page .tpl-head h2 { margin: 0; }

#tpl-list {
  display: flex;
  flex-direction: column;
  gap: .65rem;
}

.tpl-card {
  padding: .85rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.tpl-card.inactive { opacity: .75; }
.tpl-card-head {
  display: flex;
  align-items: center;
  gap: .45rem;
  flex-wrap: wrap;
}
.tpl-card-name { flex: 1; font-size: .98rem; }
.tpl-card-meta {
  margin: .4rem 0 0;
  font-size: .9rem;
}
.tpl-card-facts {
  margin: .25rem 0 0;
  font-size: .78rem;
  color: var(--muted);
}
.tpl-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  margin-top: .55rem;
  flex-wrap: wrap;
}
.tpl-card-when { font-size: .74rem; color: var(--muted); }
.tpl-card-actions { display: flex; gap: .75rem; }
.tpl-card-actions .btn-link { font-size: .8rem; min-height: 30px; }
.tpl-del { color: var(--danger); }
.tpl-del:disabled { opacity: .4; cursor: not-allowed; }

/* The editor form — the same fields as the compose form, same styling. */
.templates-page #tpl-form {
  display: flex;
  flex-direction: column;
  gap: .3rem;
}
.templates-page #tpl-form label {
  margin-top: .5rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--muted);
}
.templates-page #tpl-form select,
.templates-page #tpl-form input[type="text"],
.templates-page #tpl-form textarea {
  width: 100%;
  padding: .55rem .7rem;
  font-size: .92rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
}
.templates-page #tpl-form textarea {
  resize: vertical;
  min-height: 80px;
}
