/*
 * TreeScribe field stylesheet.
 *
 * Dropped legacy classes:
 * active, badge, big, bl, br, cam, cardrow, cell, del, done, fixwarn,
 * good, gps-far, gps-fix, gps-near, gpslive, grid, gridwrap, harvestbox,
 * hvhold, hvlog, hvresult, ic, jump, missed, muted, noteact, noteacts,
 * notebody, notechev, notedel, notehead, noteitem, notelabel, notelog,
 * notephoto, notephotogrid, notephotos, notesect, notetext, on, pedchev,
 * pedmissing, pednote, pedself, poor, prevhead, prevmeta, prevplant,
 * prevsect, prevtag, prevwhy, priorbody, rateundo, replantbox, results,
 * rollopt, rowplant, rpchosen, rphead, rplog, rppn, rprow, rptype, rpwas,
 * scancorner, secthead, spacer, svdist, svhold, svwarn, sync-state, synced,
 * syncstrip,
 * tabbar, tarrow, tcycle, tdot, tdots, thumb, thumbs, tihead, tirow,
 * tisect, tititle, tl, tr, traitstart, tsel, unknown, unsynced,
 * unverified, uploadpage, viewer-btn, viewer-name, warn, warnbtn, warnx,
 * working.
 */

:root {
  color-scheme: light;
  --green: #176a37;
  --green-bright: #22894a;
  --marked: rgba(214,51,197,0.95);
  /* WHAT "THIS REACHED DRIVE" IS COLORED, WHICH IS NOT THE SAME THING AS THE ACCENT. The two used to
     be one token, so a button and a verified state were the same green and neither meant anything by
     being green. Splitting them is what lets the accent change without "Everything is in Drive"
     losing the one color that says it is good news. */
  --success: #176a37;
  /* HOW AN ACTION BUTTON IS FILLED, AS A TOKEN, because the two themes fill one differently and the
     rules below must not have to know which theme they are in. On a light ground the accent is the
     fill and the label is drawn out of it, which is what this app has always done. */
  --accent-fill: #176a37;
  --accent-edge: #22894a;
  --accent-label: #ffffff;
  --bg: #f3efe6;
  --panel: #ffffff;
  --panel-2: #ece6da;
  --line: #cfc6b5;
  --text: #211d18;
  --muted: #5c5347;
  --danger: #b4291d;
  --warn: #8a5e0f;
  --on-accent: #ffffff;
  --overlay: rgba(33, 29, 24, 0.48);
  --overlay-half: rgba(33, 29, 24, 0.50);
  --overlay-strong: rgba(33, 29, 24, 0.58);
  --shadow: rgba(33, 29, 24, 0.28);
  --shadow-soft: rgba(33, 29, 24, 0.14);
  --shadow-faint: rgba(33, 29, 24, 0.08);
  --shadow-side: rgba(33, 29, 24, 0.18);
  --media-overlay: rgba(0, 0, 0, 0.72);
  --media-text: #ffffff;
  --toast-bg: #000000;
  --toast-text: #ffffff;
  --viewer-bg: #000000;
  --viewer-panel: #111111;
  --viewer-control: #333333;
  --viewer-text: #f0ece4;
  --viewer-muted: #b3aa9c;
  --viewer-line: #444444;
  --viewer-control-line: #555555;
  --scanner-frame: #ffffff;
  --scanner-bg: #000000;
  --scanner-mask: rgba(0, 0, 0, 0.34);
  --scanner-shadow: #000000;
  --scanner-miss: #ff6f61;
  --frozen-bg: #fff1f0;
  /* A neutral white flash provides familiar action feedback.
     The wash still means the record is safely stored on the phone, not just that a tap registered.
     These existing white and black tokens stay high contrast in every theme, including sunlight. */
  --flash-success: var(--media-text);
  --flash-danger: #7a1710;
  --flash-text: var(--scanner-bg);
  --line-width: 1px;
  --font-weight-body: 400;
  --font-weight-heading: 700;
  --pressed-tint: var(--panel-2);
}

html[data-theme="dark"] {
  color-scheme: dark;
  /* SLATE, NOT GREEN, ON THE DARK GROUND. The mint accent shipped before 2026-09-08 had a
     saturated green hairline that read, on a very warm, very dark ground, as a lit wire rather than an
     edge. Slate is the one
     candidate with no counterpart anywhere else in either palette: the bone tone that scored highest
     on contrast is within a hair of the light theme's own --line, so it would have been an accent in
     one theme and a divider in the other. Contrast on this ground is 7.19:1 for the accent and
     9.36:1 for the bright, both well past AA. */
  --green: #8aa4be;
  --green-bright: #a2bbd4;
  --marked: rgba(214,51,197,0.95);
  --success: #4fbd76;
  --bg: #15130f;
  --panel: #201c17;
  --panel-2: #2b2620;
  --line: #4a4238;
  --text: #f1ebe0;
  --muted: #b8ad9c;
  --danger: #ff776d;
  --warn: #f3b34c;
  --on-accent: #15130f;
  /* AND ON THE DARK GROUND THE FILL IS THE ACCENT AT A SIXTH OF ITS STRENGTH, with the accent itself
     as the label and a stronger accent edge around it. Six treatments were compared on
     2026-09-08; the solid fill was unsuitable because a saturated block of color on a very
     dark ground is a mass rather than a control, and the hairline around it was
     hard to distinguish. The tint keeps a filled shape a thumb can aim at without the color mass. */
  --accent-fill: rgba(138, 164, 190, 0.16);
  --accent-edge: rgba(138, 164, 190, 0.42);
  --accent-label: #a2bbd4;
  --overlay: rgba(0, 0, 0, 0.72);
  --overlay-half: rgba(0, 0, 0, 0.76);
  --overlay-strong: rgba(0, 0, 0, 0.82);
  --shadow: rgba(0, 0, 0, 0.62);
  --shadow-soft: rgba(0, 0, 0, 0.44);
  --shadow-faint: rgba(0, 0, 0, 0.32);
  --shadow-side: rgba(0, 0, 0, 0.48);
  --media-overlay: rgba(0, 0, 0, 0.82);
  --media-text: #ffffff;
  --toast-bg: #f1ebe0;
  --toast-text: #15130f;
  --viewer-bg: #000000;
  --viewer-panel: #111111;
  --viewer-control: #333333;
  --viewer-text: #f0ece4;
  --viewer-muted: #b3aa9c;
  --viewer-line: #444444;
  --viewer-control-line: #555555;
  --scanner-frame: #ffffff;
  --scanner-bg: #000000;
  --scanner-mask: rgba(0, 0, 0, 0.34);
  --scanner-shadow: #000000;
  --scanner-miss: #ff6f61;
  --frozen-bg: #321a17;
  --flash-success: var(--media-text);
  --flash-danger: #7a1710;
  --flash-text: var(--scanner-bg);
  --line-width: 1px;
  --font-weight-body: 400;
  --font-weight-heading: 700;
  --pressed-tint: #3a332a;
}

html[data-theme="field"] {
  color-scheme: light;
  /* THE FIELD THEME KEEPS ITS GREEN. It exists to be read in direct sun, where the accent is carrying
     contrast rather than character, and slate at this lightness would not survive that. */
  --green: #0a7d2c;
  --green-bright: #006b24;
  --marked: rgba(214,51,197,0.95);
  --success: #0a7d2c;
  /* AND IT KEEPS THE SOLID FILL WITH IT, for the same reason: a tint read at arm's length in direct
     sun is not a control, it is a smudge. */
  --accent-fill: #0a7d2c;
  --accent-edge: #006b24;
  --accent-label: #ffffff;
  --bg: #ffffff;
  --panel: #ffffff;
  --panel-2: #f2f2f2;
  --line: #000000;
  --text: #000000;
  --muted: #1a1a1a;
  --danger: #d00000;
  --warn: #b35c00;
  --on-accent: #ffffff;
  /* THE SCRIM IS A DARK WASH, NOT A SHEET OF THE SAME COLOR AS THE SHEET. These three read
     #f2f2f2, the same value as --panel-2, so a white sheet sat on a scrim it differed from by a
     contrast ratio of about 1.06 to 1, and the fully opaque value erased the screen behind it: the
     one theme built for glare gave a sheet the least separation from its surroundings of any
     theme, and took away the cue that a sheet is a layer over something. A strong translucent
     black puts the white sheet at better than six to one against its own scrim and leaves the
     screen behind faintly readable. */
  --overlay: rgba(0, 0, 0, 0.62);
  --overlay-half: rgba(0, 0, 0, 0.68);
  --overlay-strong: rgba(0, 0, 0, 0.74);
  --shadow: transparent;
  --shadow-soft: transparent;
  --shadow-faint: transparent;
  --shadow-side: transparent;
  --media-overlay: #000000;
  --media-text: #ffffff;
  --toast-bg: #000000;
  --toast-text: #ffffff;
  --viewer-bg: #ffffff;
  --viewer-panel: #ffffff;
  --viewer-control: #f2f2f2;
  --viewer-text: #000000;
  --viewer-muted: #1a1a1a;
  --viewer-line: #000000;
  --viewer-control-line: #000000;
  --scanner-frame: #ffffff;
  --scanner-bg: #000000;
  --scanner-mask: #000000;
  --scanner-shadow: #000000;
  --scanner-miss: #d00000;
  --frozen-bg: #ffffff;
  --flash-success: var(--media-text);
  --flash-danger: #d00000;
  --flash-text: var(--scanner-bg);
  --line-width: 2px;
  --font-weight-body: 600;
  --font-weight-heading: 800;
  --pressed-tint: #f2f2f2;
}

@media (prefers-color-scheme: dark) {
  html:not([data-theme]) {
    color-scheme: dark;
    /* The same slate the explicit dark theme takes, because a phone set to system dark and a phone
       set to dark are the same phone as far as anybody looking at it is concerned. */
    --green: #8aa4be;
    --green-bright: #a2bbd4;
    --marked: rgba(214,51,197,0.95);
    --success: #4fbd76;
    /* The same tinted fill the explicit dark theme takes, for the same reason. */
    --accent-fill: rgba(138, 164, 190, 0.16);
    --accent-edge: rgba(138, 164, 190, 0.42);
    --accent-label: #a2bbd4;
    --bg: #15130f;
    --panel: #201c17;
    --panel-2: #2b2620;
    --line: #4a4238;
    --text: #f1ebe0;
    --muted: #b8ad9c;
    --danger: #ff776d;
    --warn: #f3b34c;
    --on-accent: #15130f;
    --overlay: rgba(0, 0, 0, 0.72);
    --overlay-half: rgba(0, 0, 0, 0.76);
    --overlay-strong: rgba(0, 0, 0, 0.82);
    --shadow: rgba(0, 0, 0, 0.62);
    --shadow-soft: rgba(0, 0, 0, 0.44);
    --shadow-faint: rgba(0, 0, 0, 0.32);
    --shadow-side: rgba(0, 0, 0, 0.48);
    --media-overlay: rgba(0, 0, 0, 0.82);
    --media-text: #ffffff;
    --toast-bg: #f1ebe0;
    --toast-text: #15130f;
    --viewer-bg: #000000;
    --viewer-panel: #111111;
    --viewer-control: #333333;
    --viewer-text: #f0ece4;
    --viewer-muted: #b3aa9c;
    --viewer-line: #444444;
    --viewer-control-line: #555555;
    --scanner-frame: #ffffff;
    --scanner-bg: #000000;
    --scanner-mask: rgba(0, 0, 0, 0.34);
    --scanner-shadow: #000000;
    --scanner-miss: #ff6f61;
    --frozen-bg: #321a17;
    --flash-success: var(--media-text);
    --flash-danger: #7a1710;
    --flash-text: var(--scanner-bg);
    --line-width: 1px;
    --font-weight-body: 400;
    --font-weight-heading: 700;
    --pressed-tint: #3a332a;
  }
}

:root {
  --radius: 14px;
  --tap: 54px;
  --font-body: 1rem;
  --font-heading-1: 1.75rem;
  --font-heading-2: 1.35rem;
  --font-heading-3: 1.125rem;
  --line-body: 1.55;
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  /* THE ONE GAP BETWEEN A SCREEN'S PINNED CHROME AND ITS FIRST CONTENT. Every screen used to reach
     its own first gap through whatever margin or padding its first element happened to carry, so
     the trait picker's search field touched the top bar's line, the photo category question sat
     almost on it, the mode list stood 16px clear and the plant card stood 12px clear. This value is
     the plant card's own gap, settled on 2026-09-03. It is defined once
     and never per theme: the field theme's heavier lines change what a screen draws, not how far
     its content sits below the bar. Whatever is pinned above the content owns the measurement, so
     on the mode screen the gap is counted from the receiver strip rather than from the bar. */
  --screen-gap: 12px;
  /* THE ONE GAP BETWEEN THE ROWS OF A VERTICAL LIST, AND BETWEEN A LIST AND WHATEVER SITS ABOVE IT.
     The Help home used to space its search field 16 px from its rows, its rows 8 px from each
     other, and the welcome tour row that sat between them at nothing at all, so a screen made of
     one kind of row read as three unrelated blocks. This value is the mode screen's own gap, established
     across the season, and it is defined once so every list of rows in the
     app carries the same rhythm: the mode list, the trial and trait picker, the photo category
     list, the gallery list, and the Help home's search field, results and section list. */
  --list-gap: 10px;
  --pressed-scale: 0.98;
  --pressed-transition: background-color 100ms ease-out, color 100ms ease-out, transform 100ms ease-out;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  font: var(--font-body)/var(--line-body) -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-weight: var(--font-weight-body);
  overscroll-behavior-y: contain;
}

h1,
h2,
h3 {
  font-weight: var(--font-weight-heading);
}

h1,
h2,
h3,
p {
  margin: 0;
}

h1 {
  font-size: var(--font-heading-1);
  line-height: 1.2;
}

h2 {
  font-size: var(--font-heading-2);
  line-height: 1.25;
}

h3 {
  font-size: var(--font-heading-3);
  line-height: 1.3;
}

p + p {
  margin-top: var(--space-1);
}

small,
.muted-copy,
.empty-state-secondary {
  color: var(--muted);
  font-size: var(--font-body);
  line-height: var(--line-body);
}

/* THE SHELL IS EXACTLY ONE VIEWPORT TALL, SO THE SCREEN INSIDE IT IS THE ONLY THING THAT SCROLLS.
   It used to carry min-height alone, which is a floor and not a ceiling: whenever a screen's content
   was taller than the phone, the flex column simply grew and the DOCUMENT scrolled, so .scroll never
   scrolled inside itself at all and everything pinned with position: sticky rode up with the page.
   The receiver strip on the mode screen is what this cost. Measured in WebKit on 2026-09-04, the
   chestnut activity list stands 1114 px against a 932 px iPhone viewport and 1106 px against a
   667 px one; the strip is sticky at top 0, which is the same place the top bar is sticky, so the
   strip slid underneath the bar and all but a sliver of the receiver entry disappeared. The
   strip needs to remain visible while the activity list scrolls.

   With a ceiling as well as a floor, the top bar and the strip are ordinary flex items that stay
   where they are put, and .scroll and .screen scroll their own content beneath them. Every overlay
   in this file is position: fixed and #app builds no containing block for one, so overflow: hidden
   here cannot clip a sheet, a toast, the viewer or the scanner. */
.app,
#app {
  height: 100vh;
  height: 100dvh;
  min-height: 100vh;
  min-height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* A flex item's automatic minimum size is its content, which would push a long screen out of the
   shell instead of scrolling it, so both scrolling containers give that minimum up. */
.screen {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: var(--screen-gap) 14px calc(env(safe-area-inset-bottom) + 12px);
}

/* A screen with nothing pinned above it stands at the top of the page and owns the top safe area
   itself. Under a top bar the bar already holds that inset, and adding it again here is what used
   to push a screen a whole notch further down than the gap it was asking for. */
.screen:first-child {
  padding-top: max(env(safe-area-inset-top), var(--screen-gap));
}

/* "safe" centering keeps the top of the content reachable when it is taller than the box it is
   centered in; ordinary centering pushes the overflow past the scrollable start, where no amount of
   scrolling can reach it. */
.center {
  justify-content: safe center;
  align-items: center;
  text-align: center;
  gap: 16px;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 5;
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: calc(54px + env(safe-area-inset-top));
  padding: max(env(safe-area-inset-top), 10px) 12px 10px;
  background: var(--panel);
  border-bottom: var(--line-width) solid var(--line);
}

.topbar-back,
.topbar-menu {
  flex: none;
}

/* The icons are drawn by the ::before rules below; any text a screen leaves inside these buttons
   renders beside the icon as a doubled glyph. The double class outranks .iconbtn's font size,
   which silently overrode a plain single-class guard here. */
.topbar-back.topbar-back,
.topbar-menu.topbar-menu {
  font-size: 0;
}

.topbar-menu {
  margin-left: auto;
}

.topbar-back::before {
  display: block;
  width: 12px;
  height: 12px;
  border-bottom: 3px solid currentColor;
  border-left: 3px solid currentColor;
  content: "";
  transform: translateX(2px) rotate(45deg);
}

.topbar-menu::before {
  display: block;
  width: 5px;
  height: 5px;
  background: currentColor;
  border-radius: 50%;
  box-shadow: -8px 0 currentColor, 8px 0 currentColor;
  content: "";
}

.title,
.topbar .title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.title small,
.topbar .title small {
  display: block;
  overflow: hidden;
  margin-top: 2px;
  color: var(--muted);
  font-size: var(--font-body);
  font-weight: 400;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--screen-gap) 14px calc(env(safe-area-inset-bottom) + 14px);
  -webkit-overflow-scrolling: touch;
}

/* The Receiver screen mounts inside the auxiliary panel's own scrolling container, so two boxes
   carry the screen padding one inside the other. The inner one gives the gap up: it belongs to
   whichever container sits directly under the pinned chrome. */
.chrome-panel > .screen {
  padding-top: 0;
}

.brand {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: 0.2px;
}

.login-icon {
  display: block;
  width: 96px;
  height: 96px;
  margin: 0 auto var(--space-2);
  /* The artwork carries its own cream background and no size we ship has an alpha channel, so on the
     dark theme it lands as a hard bright rectangle on near-black. Rounding it reads as an app icon
     rather than a stray square. 18px is a radius this sheet already uses, so this adds no new value. */
  border-radius: 18px;
}

/* The invitation to other breeding programs. It sits under the card rather than inside it, so it
   reads as a note about the app rather than as part of signing in, and it is the first thing a
   person who has no access code can act on. */
.login-invite {
  margin: var(--space-2) auto 0;
  max-width: 34em;
  color: var(--muted);
  font-size: var(--font-body);
  line-height: var(--line-body);
  text-align: center;
}

.login-invite a {
  color: inherit;
  text-decoration: underline;
}

.brand small {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: var(--font-body);
  font-weight: 400;
}

.card {
  width: 100%;
  max-width: 520px;
  padding: 18px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.login-screen {
  min-height: 0;
}

/* The form exists for password managers; the card's stacked layout must not see it. */
.login-form {
  display: contents;
}

.error-screen {
  min-height: 70dvh;
}

.error-message,
.error {
  min-height: 1.2em;
  margin-top: 12px;
  color: var(--danger);
}

.error-actions {
  width: min(100%, 420px);
}

.hint,
.empty {
  color: var(--muted);
  font-size: var(--font-body);
}

.empty {
  padding: var(--space-3);
  text-align: center;
}

.empty-state,
.error-state {
  width: min(100%, 520px);
  margin: 0 auto;
}

/* AN EMPTY STATE CENTERS ITS OWN CONTENT RATHER THAN FLOATING ON AN AUTO TOP MARGIN. The auto
   margin put the screen's first element wherever the leftover height happened to leave it, which
   on the gallery was 250px below the top bar. Growing the box instead leaves its top at the screen
   gap, where the rule says a screen's first element belongs, and leaves what a person sees in
   exactly the same place. */
.empty-state {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 220px;
}

.empty-state-primary {
  color: var(--text);
  font-size: var(--font-heading-3);
  font-weight: 700;
  line-height: 1.35;
}

.empty-state-secondary {
  max-width: 38ch;
  margin-top: var(--space-1);
}

label {
  display: block;
  margin: 14px 0 6px;
  color: var(--muted);
  font-size: var(--font-body);
}

/* Rule 70: every rendered input type stays at 1rem so iOS never enters sticky zoom. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="checkbox"],
input[type="search"],
input[type="range"],
input[type="number"],
textarea {
  font: inherit;
  font-size: 1rem;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="number"] {
  width: 100%;
  height: var(--tap);
  padding: 0 14px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.checkbox {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
  color: var(--text);
}

.checkbox input {
  width: 22px;
  height: 22px;
  accent-color: var(--green);
}

/* HIDDEN MEANS HIDDEN, WHATEVER ELSE THIS FILE SAYS ABOUT THE ELEMENT. The browser hides an element
   with the hidden attribute through its own display rule, and any display declaration in this file
   beats it, so every control the app hides by setting element.hidden stayed on screen: .btn-primary
   and .btn-ghost both set display: flex, which is why "Send stopped photos" sat on the Sync panel
   and in the Uploads footer with nothing stopped, and why "Cancel this upload" sat under a finished
   sync with nothing uploading. The app hides a control by setting hidden in a dozen places, so the
   rule belongs here once rather than as a class each of those places has to remember. */
[hidden] {
  display: none !important;
}

button {
  font: inherit;
  cursor: pointer;
  border: var(--line-width) solid transparent;
  border-radius: var(--radius);
}

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

html[data-theme="field"] a {
  text-decoration: underline;
}

html[data-theme="field"] button.selected,
html[data-theme="field"] button[aria-pressed="true"] {
  border-width: 3px;
}

html[data-theme="field"] .success::before,
html[data-theme="field"] .green::before,
html[data-theme="field"] .collect-message.is-status::before,
html[data-theme="field"] .screen-answer-state.answered::before,
html[data-theme="field"] .accession-check-state.verified::before {
  content: "✓";
}

html[data-theme="field"] .warning::before,
html[data-theme="field"] .amber::before,
html[data-theme="field"] .status-chip.neutral::before {
  content: "•";
}

html[data-theme="field"] .danger::before,
html[data-theme="field"] .red::before,
html[data-theme="field"] .bad::before,
html[data-theme="field"] .collect-message.is-error::before,
html[data-theme="field"] .score-message::before,
html[data-theme="field"] .accession-check-state.not-checked::before {
  content: "×";
}

/* The card's status and score messages are always in the DOM and usually empty; a glyph on an
   empty message would float alone above the buttons. */
html[data-theme="field"] .collect-message:empty::before,
html[data-theme="field"] .score-message:empty::before {
  content: none;
}

html[data-theme="field"] .success::before,
html[data-theme="field"] .green::before,
html[data-theme="field"] .warning::before,
html[data-theme="field"] .amber::before,
html[data-theme="field"] .danger::before,
html[data-theme="field"] .red::before,
html[data-theme="field"] .bad::before,
html[data-theme="field"] .status-chip.neutral::before,
html[data-theme="field"] .collect-message::before,
html[data-theme="field"] .score-message::before,
html[data-theme="field"] .screen-answer-state.answered::before,
html[data-theme="field"] .accession-check-state::before {
  flex: none;
  margin-right: 6px;
  font-weight: 800;
}

button:focus-visible,
input:focus-visible,
textarea:focus-visible,
.orthomap-compass:focus-visible {
  outline: 3px solid var(--green-bright);
  outline-offset: 2px;
}

.btn-primary,
.btn-go {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--tap);
  margin-top: 20px;
  padding: 0 14px;
  background: var(--accent-fill);
  color: var(--accent-label);
  border-color: var(--accent-edge);
  font-size: 1.05rem;
  font-weight: 700;
}

/* A TINTED CONTROL CANNOT BE DISABLED BY FADING IT. Half of a sixteen-percent fill is eight percent,
   which is nothing, and the label fades with it. "Syncing now..." IS the disabled state of the
   button a person watches while a pass runs, so it has to stay readable. A disabled control gives up
   the accent instead: it keeps its shape and its words and it is plainly not the thing to press. */
.btn-primary:disabled,
.btn-go:disabled {
  background: var(--panel-2);
  color: var(--muted);
  border-color: var(--line);
}

.btn-ghost {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--tap);
  margin-top: 10px;
  padding: 0 14px;
  background: var(--panel-2);
  color: var(--text);
  border-color: var(--line);
}

/* THE GLYPH IN FRONT OF A CARD BUTTON'S WORDS, AND THE WORDS BESIDE IT. The glyph is decoration:
   the markup marks it aria-hidden, and this rule keeps it out of the label's way rather than
   letting the two share the button's centering. It is set larger than the words because a mark read
   at arm's length in sunlight is read by its shape. The capture button's glyph is larger still,
   which is the size the hazel field app gave the camera on that one button. */
.btnic {
  flex: none;
  margin-right: 8px;
  font-size: 1.25rem;
  line-height: 1;
}

.capture .btnic {
  font-size: 1.6rem;
}

.btnlabel {
  min-width: 0;
}

.iconbtn {
  min-width: 44px;
  min-height: 44px;
  padding: 0 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  background: var(--panel-2);
  color: var(--text);
  border-color: var(--line);
  border-radius: 10px;
  font-size: 1.05rem;
}

.chip,
.pedchip {
  flex: none;
  min-height: 44px;
  padding: 0 14px;
  background: var(--panel);
  color: var(--green);
  border: var(--line-width) solid var(--line);
  border-radius: 999px;
  font-size: var(--font-body);
  font-weight: 700;
  white-space: nowrap;
}

.crop-chip,
.plant-chip,
.definition-chip,
.traitinfo {
  align-self: center;
}

.full-width {
  width: 100%;
}

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom) + 74px);
  z-index: 50;
  display: -webkit-box;
  box-sizing: border-box;
  width: max-content;
  max-width: 90vw;
  max-height: calc(3 * 1.4em + 32px);
  padding: 16px;
  overflow: hidden;
  transform: translateX(-50%);
  background: var(--toast-bg);
  color: var(--toast-text);
  border-radius: 12px;
  opacity: 0;
  pointer-events: none;
  text-align: center;
  line-height: 1.4;
  white-space: normal;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  transition: opacity 0.2s;
}

.toast-notice {
  border-radius: 12px;
}

.toast.show {
  opacity: 0.94;
}

/* Menu sheet. A sheet SLIDES UP from the bottom edge over about 220ms while the scrim fades in,
   and leaves the same way in reverse over about 160ms; before 2026-09-03 it
   snapped into existence without a transition. public/app/ui/menu.js adds is-open on the animation frame
   after the sheet mounts and is-closing when it starts to leave, then removes the overlay on the
   sheet's own transitionend or on its fallback timer, whichever arrives first.

   A SHEET THAT REPLACES ANOTHER DOES NOT REPEAT THE SCRIM FADE. Settings opening from the main
   menu, a chooser opening from Settings and Back returning all re-use the overlay that is already
   mounted, and that overlay keeps is-open, so the scrim's computed color never changes and the
   screen behind it does not flash. Only the new sheet, which mounts without is-open of its own,
   moves. A closing overlay stops taking taps, so nothing about the animation can swallow one.

   A SHEET THAT REPLACES ANOTHER ALSO MOVES SIDEWAYS RATHER THAN UP. Only a fresh open comes from
   the bottom edge and only a real close goes back down to it. Drilling in, which is Settings from
   the menu or a chooser from Settings, brings the new sheet from the right while the sheet it
   replaces leaves to the left; Back reverses both. On 2026-09-03, Back
   inside the menu used to swipe up from the bottom again, which reads as a new sheet arriving
   rather than as a step backwards. The sheet on its way out is taken out of the flow so the sheet
   replacing it holds the overlay's own place, and it takes no taps while it goes. */
.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-end;
  padding: max(env(safe-area-inset-top), 12px) 12px max(env(safe-area-inset-bottom), 12px);
  background: transparent;
  transition: background-color 220ms ease-out;
}

.menu-overlay.is-open {
  background: var(--overlay);
}

.menu-overlay.is-closing {
  background: transparent;
  pointer-events: none;
  transition: background-color 160ms ease-out;
}

.menu-sheet {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-height: min(75dvh, calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 24px));
  overflow: hidden;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 18px;
  box-shadow: 0 16px 48px var(--shadow);
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  transform: translateY(100%);
  transition: transform 220ms ease-out;
}

.menu-sheet.is-open {
  transform: translateY(0);
}

.menu-sheet.sheet-from-right:not(.is-open) {
  transform: translateX(100%);
}

.menu-sheet.sheet-from-left:not(.is-open) {
  transform: translateX(-100%);
}

.menu-sheet.sheet-leaving {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: max(env(safe-area-inset-bottom), 12px);
  pointer-events: none;
}

.menu-sheet.sheet-leaving.sheet-to-left {
  transform: translateX(-100%);
}

.menu-sheet.sheet-leaving.sheet-to-right {
  transform: translateX(100%);
}

.menu-overlay.is-closing .menu-sheet {
  transform: translateY(100%);
  transition: transform 160ms ease-out;
}

/* THE SHEET IS NOT THE APP UNDERNEATH IT, AND IT SAYS SO IN THE ONE COLOR THE APP ALREADY OWNS.
   A tint cannot do this job, because the high contrast theme is pure black on pure white by design
   and a wash disappears in it; these two were selected after comparing eleven treatments
   rendered in all three themes. The bar sits on the sheet's own top edge and the edge on each row
   repeats it down the list, so in high contrast the two read as one frame and in the other two as
   an accent. Both are structural rather than tonal, which is why they survive every theme. */
/* THE SHEET IS MARKED BY AN EDGE, NOT BY A SLAB. This was a six pixel bar in the accent across the
   top of the sheet, with a four pixel rail down the left of every entry inside it. In green on a
   pale ground that read as decoration; in slate on the dark ground it read as nine broken borders,
   because the rail is straight and the row it sits against is rounded, so each entry wore an arc of
   color on one side only. This was observed on 2026-09-08 after the accent change. */
.menu-sheet {
  border-top: 2px solid var(--accent-edge);
}

/* AND THE ENTRY THAT IS ALREADY CHOSEN IS THE ONE THING IN THE SHEET THAT WEARS THE ACCENT, in the
   same tinted fill an action button takes, so a person reads which walk order is in use from across
   the row rather than from a check mark the width of a letter. Every other entry is a control they
   might press, which is what the plain treatment says. */
.menu-item.selected {
  background: var(--accent-fill);
  color: var(--accent-label);
  border-color: var(--accent-edge);
}

.menu-sections {
  min-height: 0;
  overflow-y: auto;
  padding: 10px 12px 12px;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.menu-footer {
  flex: none;
  padding: 0 12px 12px;
  background: var(--panel);
  border-top: var(--line-width) solid var(--line);
}

/* EVERY ENTRY WEARS A FULL OUTLINE IN THE ACCENT. Two earlier treatments failed on 2026-09-08:
   the four pixel rail down one side read as nine broken arcs in slate, and removing the rail left
   only the hairline every button has, which was indistinguishable. The outline goes all the way round,
   in the accent
   edge, on every theme; the selected entry keeps the tinted fill inside it. */
.menu-item {
  justify-content: flex-start;
  margin-top: 8px;
  text-align: left;
  border: 2px solid var(--accent-edge);
}

.menu-cancel {
  justify-content: center;
  margin-top: 10px;
  border-width: 2px;
}

/* TWO ENTRIES SIDE BY SIDE ON ONE LINE, each half the width and each the full height of a row, so
   neither half is a smaller tap target than any other entry in the sheet. The line carries the gap
   above it and the two buttons inside carry none of their own, so a pair costs one row's height
   however many entries share it. */
.menu-pair {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}

.menu-pair .menu-item {
  flex: 1 1 0;
  min-width: 0;
  margin-top: 0;
}

.menudiv {
  margin: 14px 0 4px;
  border-top: var(--line-width) solid var(--line);
}

.menu-heading {
  margin: 8px 2px 2px;
  color: var(--muted);
  font-size: var(--font-body);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* THE PAGE MENU IS THE ONE SHEET THAT HOLDS A WHOLE LIST AT ONCE, and it has to hold it on the
   shortest phone the crew carries without scrolling, because on iOS a tap that lands while a
   scrollable container is still decelerating stops the scroll and produces no click at all. That is
   why the Settings item ignored the first tap.

   WHAT WAS MEASURED, IN A BROWSER, ON 2026-09-05. Eleven entries, four section headings and three
   hairlines at 46px rows stood 727px tall. The 2026-09-04 wave had called that a fit, but it had
   measured only an iPhone 16 Pro and only against a cap it had itself raised from 75dvh to the whole
   height the safe areas leave: 727px against 815px. On the same phone the sheet was 78 percent of
   the screen, which the 75 percent rule forbids, and on an iPhone
   SE the same 727px stood against a 623px cap and scrolled.

   THE ARITHMETIC THE ANSWER HAD TO SATISFY. An iPhone SE leaves the sheet 623px. Eleven rows at the
   44px an iPhone counts as a tap target are 484px of that on their own, and the footer, the two
   sheet borders and the scroller's padding take about 65px more, so a sheet that also draws four
   1rem headings and three hairlines cannot fit however tightly its margins are set. One line had to
   go, and no entry could: Row by row and Serpentine are the two halves of one choice, so they share
   a line in public/app/main.js and both are still there, still labeled, still tappable. With that
   line saved and the measurements below, the same browser reads the sheet at 596px: 27px inside the
   iPhone SE's cap, and 64 percent of an iPhone 16 Pro's screen, which is inside the 75 percent rule
   as well.

   None of this reaches a Settings sub-sheet: they hold a handful of rows each and keep the shared
   measurements. */
.page-menu-sheet {
  max-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 24px);
}

.page-menu-sheet .menu-sections {
  padding: 4px 12px 4px;
}

.page-menu-sheet .menu-heading {
  margin: 3px 2px 0;
  line-height: 1.1;
}

.page-menu-sheet .menudiv {
  margin: 4px 0 0;
}

.page-menu-sheet .menu-item {
  min-height: 44px;
  margin-top: 4px;
}

.page-menu-sheet .menu-pair {
  margin-top: 4px;
}

.page-menu-sheet .menu-footer {
  padding: 0 12px 4px;
}

.page-menu-sheet .menu-cancel {
  min-height: 44px;
  margin-top: 4px;
}

.menu-sheet-message {
  margin: 12px 2px 4px;
  line-height: 1.45;
}

/* THE ONE SETTINGS ROW WHOSE TEXT IS MEASURED FROM THE PHONE, AND THE HEIGHT IT KEEPS WHETHER OR
   NOT THAT MEASUREMENT HAS ARRIVED. "Free up space. 24 photos (0.23 GB) stored locally." wraps onto
   a second line on every phone the crew carries, so a row that started at one line and grew to two
   pushed everything below it down by about twenty pixels. On 2026-09-04 the operator's taps on the
   two rows immediately below it produced no click at all, which is what iOS does when the element
   under a finger moves between the press and the release. The row is two lines tall from the moment
   it is drawn, so nothing below it can be moved by a measurement landing late. */
/* THE ROW RESERVES THREE LINES BECAUSE ITS SENTENCE NOW NAMES TWO NUMBERS. It used to say only what
   the phone was holding and fitted in two; it says what is held and what can be cleared, which runs
   to three lines on a phone, and the whole point of the reserve is that the row is the same height
   before and after a cold measurement lands. */
.settings-storage-row {
  align-items: flex-start;
  white-space: normal;
  line-height: 1.35;
  padding-top: 12px;
  padding-bottom: 12px;
  min-height: calc(4.05em + 24px);
}

.menu-section,
.menu-section-trial,
.menu-section-data,
.menu-section-navigation,
.menu-section-account {
  scroll-margin-top: 10px;
}

/* Lists and mode selection */
.list {
  display: flex;
  flex-direction: column;
  gap: var(--list-gap);
  padding: 0 0 12px;
}

/* A list that opens a screen takes its gap from the scrolling container. Every other list keeps,
   as a margin, the space its own top padding used to put above it, so the rhythm inside the trait
   picker and the photo category screen is unchanged. */
#traitList > .list {
  margin-top: 12px;
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: var(--tap);
  padding: 14px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  text-align: left;
}

.row.ghead {
  background: var(--panel-2);
}

.grow {
  flex: 1;
  min-width: 0;
}

.grow b,
.trait-choice b {
  display: block;
  overflow-wrap: anywhere;
}

.grow small,
.trait-choice small {
  display: block;
  color: var(--muted);
}

.chev,
.disclosure-chevron {
  flex: none;
  width: 11px;
  height: 11px;
  margin-right: 4px;
  overflow: hidden;
  color: transparent;
  border-top: 2px solid var(--muted);
  border-right: 2px solid var(--muted);
  font-size: 0;
  transform: rotate(45deg);
  transition: transform 140ms ease-out;
}

.disclosure-row {
  align-items: center;
}

.disclosure-label {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}

.disclosure-row[aria-expanded="true"] .disclosure-chevron {
  transform: rotate(135deg);
}

.traitrow {
  align-items: stretch;
}

.trait-choice {
  min-height: 52px;
  padding: 4px 0;
  background: transparent;
  color: var(--text);
  border: 0;
  text-align: left;
}

/* THE CHOSEN TRAIT IS MARKED THE WAY THE CHOSEN MENU ENTRY IS: an even outline all the way round and the
   tinted fill inside it, not a rail down one side. Same reason, same day (2026-09-08): a one-sided
   accent stripe against a rounded row reads as a broken edge in slate. */
.traitrow.selected {
  background: var(--accent-fill);
  border: 2px solid var(--accent-edge);
  border-radius: var(--radius);
}

.traitrow.selected .trait-choice {
  color: var(--accent-label);
}

/* Inheriting keeps details readable when the shared pressed treatment changes the label. */
.traitrow.selected .trait-choice small {
  color: inherit;
}

.trait-order {
  display: inline-grid;
  place-items: center;
  min-width: 26px;
  height: 26px;
  margin-right: 8px;
  background: var(--green);
  color: var(--on-accent);
  border-radius: 50%;
  font-weight: 700;
}

.trait-start {
  width: 100%;
}

.trait-group,
.search-results {
  width: 100%;
}

/* THE TRAIT PICKER'S SEPARATOR ROW, which is also its lab-and-image disclosure. It is drawn in its
   own tone in BOTH states, so that before anyone taps it, it reads as the divider between the field
   traits and the lab, image and bench traits rather than as one more trait group: the panel-2
   surface the group headings use, the muted text color, no side borders, and a line above and below
   that is twice the theme's own line weight (2px in light and dark, 4px in the field theme, whose
   --line-width is the 2px line). The gap is what keeps the chevron off the end of the label: the
   two used to share one box, which drew the glyph over the last letters of "traits" at larger
   Dynamic Type sizes. */
.trait-advanced-separator {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 10px 0;
  background: var(--panel-2);
  color: var(--muted);
  border: 0;
  border-top: calc(var(--line-width) * 2) solid var(--line);
  border-bottom: calc(var(--line-width) * 2) solid var(--line);
  border-radius: 0;
  font-weight: var(--font-weight-heading);
}

.modelist {
  display: flex;
  flex-direction: column;
  gap: var(--list-gap);
}

/* THE RECEIVER STRIP SCROLLS WITH THE ACTIVITY LIST (settled 2026-09-04). It was pinned under the top
   bar for a while, held there by position sticky and
   drawn outside the scrolling region; what that produced in the hand was a band riding over a list
   somebody was scrolling. It is the first row of the list again, in its own surface so it still
   reads as a thing apart from the activities under it. The shell change that made #app exactly one
   viewport tall stays: that is what makes every screen scroll inside itself. */
.mode-receiver-strip {
  /* The surface and the line are pulled out to the edges so they run the full width, as they did
     when the strip was pinned. It used to carry no top padding at all, on the reasoning that the
     scrolling region already gives a screen gap above it; on the iPad, where the region is taller
     and the panel is wider, that left the control sitting on the panel's own top edge
     (observed 2026-09-07). */
  padding: 12px 14px;
  margin: 0 -14px var(--screen-gap);
  background: var(--panel-2);
  border-bottom: var(--line-width) solid var(--line);
}

/* Trial photos follow the activities, with the same touch target and surface as Receiver above. */
.mode-gallery-strip {
  padding: 12px 14px;
  margin: var(--screen-gap) -14px 0;
  background: var(--panel-2);
  border-top: var(--line-width) solid var(--line);
}

.modebtn {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 76px;
  padding: 16px 14px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  text-align: left;
}

.modeic {
  flex: none;
  font-size: 1.6rem;
}

.modetxt {
  flex: 1;
  min-width: 0;
}

.modetxt b {
  display: block;
  font-size: 1.05rem;
}

.modetxt small {
  color: var(--muted);
  font-size: var(--font-body);
}

.receiver-action .modeic {
  color: var(--muted);
}

/* Collection shell */
.collect-topbar {
  flex: none;
}

.syncstrip {
  display: none;
  width: 100%;
  min-height: 38px;
  padding: 7px 14px;
  background: var(--panel-2);
  color: var(--green);
  border: 0;
  border-bottom: var(--line-width) solid var(--line);
  border-radius: 0;
  font-size: var(--font-body);
  font-weight: 700;
  text-align: center;
}

.syncstrip.show {
  display: block;
}

.syncstrip.working {
  color: var(--warn);
}

.photo-category-screen,
.bulk-upload-screen {
  width: min(100%, 640px);
  margin: 0 auto;
}

.category-new {
  margin-top: 18px;
}

.category-new .btn-primary {
  margin-top: 8px;
}

.category-list {
  display: flex;
  flex-direction: column;
  gap: var(--list-gap);
  margin-top: 12px;
}

.category-row {
  width: 100%;
}

.badge {
  padding: 2px 7px;
  background: var(--panel-2);
  color: var(--green);
  border: var(--line-width) solid var(--line);
  border-radius: 999px;
  font-size: var(--font-body);
}

.big {
  min-height: 58px;
}

.plant-photo-count {
  color: var(--muted);
}

.bulk-upload-screen > .btn-primary {
  width: 100%;
}

.bulk-rtk {
  margin-top: 22px;
}

.walk-memo,
.bulk-progress,
.bulk-receipt {
  margin-top: 22px;
  padding: 14px;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.walk-memo h2,
.bulk-receipt h2 {
  margin: 0 0 8px;
  font-size: 1rem;
}

.walk-memo textarea {
  width: 100%;
  box-sizing: border-box;
  margin-top: 8px;
  padding: 10px;
  background: var(--bg);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  font-size: 1rem;
}

.bulk-track {
  height: 8px;
  margin-top: 10px;
  overflow: hidden;
  background: var(--line);
  border-radius: 6px;
}

.bulk-bar {
  width: 0;
  height: 100%;
  background: var(--green);
  transition: width 200ms ease-out;
}

.bulk-receipt ul {
  padding: 0;
  list-style: none;
}

.bulk-receipt li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
  border-bottom: var(--line-width) solid var(--line);
}

.thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: 8px;
  margin-top: 12px;
}

.thumbs .thumb {
  position: relative;
  min-height: 76px;
  overflow: hidden;
  padding: 0;
  background: var(--panel-2);
  border-color: var(--line);
}

.thumbs .thumb img {
  width: 100%;
  height: 76px;
  object-fit: cover;
}

.thumbs .photo-sync-badge,
.thumbs .synced,
.thumbs .unsynced {
  display: block;
  padding: 2px 4px;
  background: var(--media-overlay);
  color: var(--media-text);
  border-radius: 4px;
  font-size: var(--font-body);
}

/* A PHOTOGRAPH CONFIRMED IN DRIVE IS GOOD NEWS, NOT AN ACCENT. This one was missed when --success
   was split out of --green on 2026-09-08: left on the accent it would have turned slate on the dark
   theme, and a badge that says "In Drive" has to keep the one color that means the work is safe. */
.thumbs .photo-sync-badge.synced {
  background: var(--success);
  color: var(--on-accent);
}

.photo-empty {
  grid-column: 1 / -1;
}

/* THE REST OF A PLANT'S PHOTOGRAPHS, AS A COUNT. The card draws the newest few and says how many
   more the Gallery holds, so a plant with hundreds costs the same to enter as a plant with three. */
.thumb-more {
  grid-column: 1 / -1;
  padding: var(--space-1) 0;
  font-size: var(--font-body);
}

/* Equal tracks keep selection from shifting the row. The text-sized minimum preserves complete
   words when text is enlarged, with scrolling available after all four tracks outgrow the phone. */
.bottom-bar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  display: grid;
  grid-template-columns: repeat(4, minmax(4.5em, 1fr));
  flex: none;
  gap: 0;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 6px 2px calc(env(safe-area-inset-bottom) + 6px);
  background: var(--panel);
  color: var(--text);
  border-top: var(--line-width) solid var(--line);
  /* Compact navigation text fits a 320px phone without shrinking the touch targets. */
  font-size: clamp(0.875rem, 4vw, var(--font-body));
}

.bottom-bar button {
  min-width: 0;
  min-height: 54px;
  margin: 0;
  padding: 4px 2px;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  color: var(--muted);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  font-size: inherit;
  font-weight: 600;
  line-height: 1.15;
  text-align: center;
  text-decoration: none;
  /* No destination label contains a space any more, so nothing wraps; this stays permissive so
     that an enlarged text setting can wrap rather than clip a word. */
  white-space: normal;
}

.bottom-bar button.selected {
  color: var(--green);
  /* The rail marks selection without adding a box or changing the label's metrics. */
  box-shadow: inset 0 -3px 0 var(--green);
}

/* A HAIRLINE BETWEEN DESTINATIONS, ON A PSEUDO-ELEMENT ON PURPOSE. A border or a second inset
   shadow would either shift every label by a pixel or be overwritten by the selected item's rail,
   which is itself an inset shadow. This draws beside the button without entering its box model.
   It stops short at both ends so the row reads as separated rather than ruled into four cells, and
   the lower gap clears the three pixel selection rail. */
.bottom-bar button + button::before {
  content: "";
  position: absolute;
  left: 0;
  top: 22%;
  bottom: 28%;
  width: var(--line-width);
  background: var(--line);
  pointer-events: none;
}

.bottom-bar button:focus-visible {
  /* Keep the keyboard focus ring inside the scroll container at either edge of the phone. */
  outline-offset: -3px;
}

.chrome-panel {
  display: flex;
  flex-direction: column;
}

.utility-panel {
  justify-content: center;
  gap: var(--space-2);
  width: min(100%, 620px);
  margin: 0 auto;
}

.utility-intro,
.utility-status {
  line-height: var(--line-body);
}

/* THE UPLOAD SCREEN. Unlike the other utility panels it is a list that can be long, so it starts
   at the top and scrolls rather than sitting centered in the panel. */
.uploads-panel {
  justify-content: flex-start;
}

/* THE ONE LIVE SENTENCE ABOUT THE WHOLE PHONE, at the top of the upload screen where the Sync
   panel's outcome used to be. It reads at heading weight because it is the answer a person opens
   this screen for, and it takes the same three state colors and the same field-theme glyphs every
   other status line in the app takes. */
.upload-headline {
  color: var(--text);
  font-size: var(--font-heading-3);
  font-weight: var(--font-weight-heading);
  line-height: var(--line-body);
}

.upload-headline.success {
  color: var(--success);
}

.upload-headline.warning {
  color: var(--warn);
}

.upload-headline.danger {
  color: var(--danger);
}

/* THE INSTRUCTION THAT STANDS FOR AS LONG AS IT IS TRUE. It was a toast that disappeared
   while the instruction still applied; here it is part of the screen, so it is set apart
   from the counts above it by a rule and a little air rather than by shouting. */
.upload-keep-open {
  padding: var(--space-1) 0;
  border-top: var(--line-width) solid var(--line);
  border-bottom: var(--line-width) solid var(--line);
  color: var(--text);
  line-height: var(--line-body);
}

/* NOTHING LEFT TO UPLOAD IS THE ANSWER TO THIS SCREEN'S QUESTION, so it is drawn as the same
   centered headline the Gallery and the Sync panel use. It borrows .empty-state for the centering
   and .empty-state-primary for the weight and the text color, and gives up only the growing 220px
   box: this panel starts at the top and keeps the Sync control and its sentence directly under the
   headline rather than pushed to the bottom of the screen. */
.uploads-empty {
  flex: none;
  min-height: 0;
  padding: var(--space-3) 0;
}

.upload-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.upload-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2);
  border: var(--line-width) solid var(--line);
  border-radius: 14px;
  background: var(--panel);
}

.upload-row-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  align-items: baseline;
  justify-content: space-between;
}

.upload-meta {
  color: var(--muted);
  font-size: var(--font-body);
}

.upload-state {
  font-weight: var(--font-weight-heading);
}

.upload-state-failed {
  color: var(--danger);
}

.upload-state-waiting {
  color: var(--warn);
}

/* A photo a person stopped is neither an error nor work in progress; it reads as the settled thing
   it is, with its Retry now beside it. */
.upload-state-canceled {
  color: var(--muted);
}

.upload-note {
  font-size: var(--font-body);
}

.upload-row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.photo-sync-badge.verifying,
.photo-sync-badge.verifying-failed {
  background: var(--panel-2);
  color: var(--text);
}

/* THE CONTROLS THAT START THE WORK, ABOVE THE WORK. They used to sit at the foot, which on a queue
   of hundreds of rows meant a person arriving at this screen saw no way to start anything. */
.upload-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
}

/* WHAT THE LAST PASS DID SITS CLEAR OF THE LIST ABOVE IT, because it is about a pass that has ended
   rather than about what the phone is doing now, and the two ran together with nothing between
   them. */
.upload-foot {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: var(--line-width) solid var(--line);
}

/* One line per kind, and a line of air between the kinds, so Photos is found without reading the
   rest. */
.pass-report-row {
  margin-top: var(--space-1);
  line-height: var(--line-body);
}

.collect-scroll {
  display: flex;
  flex-direction: column;
}

.collect-card-region {
  width: 100%;
  max-width: 760px;
  margin: 0 auto;
}

.collect-card {
  box-shadow: 0 2px 8px var(--shadow-faint);
}

.plotcard {
  margin: 0 0 12px;
  padding: 16px;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.plotname {
  font-size: 1.5rem;
  font-weight: 800;
  overflow-wrap: anywhere;
}

.accession {
  margin-top: 3px;
  color: var(--muted);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.plotmeta {
  margin-top: 6px;
  color: var(--muted);
  font-size: var(--font-body);
}

/* THE CARD-FACE BADGE. The photography status is read at arm's length in sun, so it is bold, boxed,
   and colored by meaning: green has a matched photograph and orange explains why it does not. */
.unit-badge {
  display: inline-block;
  margin-top: 8px;
  padding: 6px 12px;
  border: var(--line-width) solid currentColor;
  border-radius: 10px;
  font-size: 1.05rem;
  line-height: 1.2;
}

.unit-badge b {
  font-weight: 800;
  letter-spacing: 0.04em;
}

.unit-badge span {
  font-weight: 600;
}

.unit-badge-count {
  color: var(--success);
}

.unit-badge-skip {
  color: var(--warn);
}

/* THE NEXT-TREE WARNING: the one line on the card that asks for a walk before a tap. */
.walk-gap {
  margin-top: 8px;
  padding: 8px 10px;
  border: var(--line-width) solid var(--warn);
  border-radius: 10px;
  color: var(--warn);
  font-weight: 700;
}

.counter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px 8px;
  margin-top: 10px;
  color: var(--muted);
  font-size: var(--font-body);
}

.navrow {
  display: flex;
  gap: 10px;
  margin-top: 10px;
}

.navrow button {
  flex: 1;
  min-height: var(--tap);
  margin-top: 0;
  background: var(--panel-2);
  color: var(--text);
  border-color: var(--line);
  font-size: 1.05rem;
  font-weight: 700;
}

.navrow button:disabled {
  opacity: 0.4;
}

.auto-row button.selected {
  background: var(--green);
  color: var(--on-accent);
  border-color: var(--green-bright);
}

.collect-message,
.score-message {
  min-height: 1.3em;
  margin-top: 8px;
  font-size: var(--font-body);
}

.collect-message,
.collect-message.is-status {
  color: var(--green);
}

.collect-message.is-error,
.score-message {
  color: var(--danger);
}

/* WHAT BREEDBASE ALREADY HOLDS FOR THIS PLANT. It is somebody else's record rather than this card's
   own state, so it is set quieter than the score and reads under the input instead of beside it. It
   must never look like a value waiting to be saved. */
.score-history {
  margin-top: 8px;
  font-size: 0.9em;
  color: var(--muted);
}

.score-history-row {
  overflow-wrap: anywhere;
}

.collect-mode-slot,
.mode-card-placeholder {
  width: 100%;
}

.mode-card-placeholder {
  padding: 18px;
  color: var(--muted);
  border: var(--line-width) dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
}

.capture {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  min-height: 84px;
  margin-top: 12px;
  background: var(--accent-fill);
  color: var(--accent-label);
  border-color: var(--accent-edge);
  font-size: 1.2rem;
  font-weight: 800;
}

.capture.survey,
.capture.harvest,
.capture.replant {
  background: var(--panel-2);
  color: var(--text);
  border: 2px solid var(--accent-edge);
}

.notebox,
.new-note {
  width: 100%;
  min-height: 72px;
  padding: 10px;
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  font: inherit;
  font-size: 1rem;
  resize: vertical;
}

.notebox.talkarea {
  min-height: 170px;
}

.notesave {
  display: block;
  width: 100%;
  min-height: var(--tap);
  margin-top: 8px;
  padding: 12px;
}

/* Scoring */
.multi-item-nav {
  display: grid;
  grid-template-columns: minmax(76px, auto) 1fr minmax(76px, auto);
  align-items: center;
  gap: 8px;
}

.multi-arrow {
  min-height: 44px;
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 8px;
}

.multi-dots {
  display: flex;
  justify-content: center;
  gap: 7px;
}

.multi-dot {
  width: 28px;
  min-width: 28px;
  height: 28px;
  min-height: 28px;
  padding: 0;
  background: transparent;
  color: var(--muted);
  border: 2px solid var(--line);
  border-radius: 50%;
  font-size: 1rem;
}

.all-done,
.find-results,
.method-group,
.multi-next,
.multi-previous,
.replant-check,
.replant-scan,
.screening-cycler,
.trait-cycler {
  min-width: 0;
}

.multi-dot.done {
  background: var(--green);
  color: var(--on-accent);
  border-color: var(--green);
}

.multi-dot.current {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}

.score-layout-all .score-trait-slot + .score-trait-slot {
  margin-top: 14px;
  padding-top: 2px;
  border-top: var(--line-width) solid var(--line);
}

.ratebox,
.score-card {
  margin-top: 12px;
  padding: 12px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.ratehead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
}

.ratename {
  font-size: 1rem;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.ratescale {
  color: var(--muted);
  font-size: var(--font-body);
}

.score-controls {
  margin-top: 8px;
}

.ratebtns {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin-top: 9px;
}

.rb {
  position: relative;
  display: flex;
  flex: 1 1 52px;
  align-items: center;
  justify-content: center;
  min-width: 52px;
  min-height: 52px;
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 12px;
  font-size: var(--font-heading-3);
  font-weight: 700;
  user-select: none;
}

.rb.sel,
.ratetick.sel {
  background: var(--green);
  color: var(--on-accent);
  border-color: var(--green-bright);
}

.ratefoot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 7px;
  color: var(--muted);
  font-size: var(--font-body);
}

.scorenum {
  display: flex;
  gap: 8px;
  margin-top: 9px;
}

.scoreinput {
  flex: 1;
  min-width: 0;
}

.scoreset {
  flex: 0 0 86px;
}

.ratebig {
  min-height: 2.4rem;
  margin-top: 4px;
  font-size: 2.1rem;
  font-weight: 800;
  line-height: 1.15;
  text-align: center;
}

.ratebig.unset {
  color: var(--muted);
}

.rateslider {
  width: 100%;
  height: 40px;
  margin: 0;
  background: transparent;
  appearance: none;
  -webkit-appearance: none;
}

.rateslider::-webkit-slider-runnable-track {
  height: 10px;
  background: var(--panel-2);
  border: var(--line-width) solid var(--line);
  border-radius: 5px;
}

.rateslider::-webkit-slider-thumb {
  width: 30px;
  height: 30px;
  margin-top: -11px;
  background: var(--green);
  border: 2px solid var(--green-bright);
  border-radius: 50%;
  appearance: none;
  -webkit-appearance: none;
}

.rateslider.unset::-webkit-slider-thumb {
  background: var(--panel);
  border-color: var(--line);
}

.rateticks {
  display: flex;
  gap: 4px;
}

.ratetick {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 4px;
  background: transparent;
  color: var(--muted);
  border: 0;
  font-size: var(--font-body);
  font-weight: 700;
  user-select: none;
}

.score-confirm {
  position: fixed;
  inset: 0;
  z-index: 65;
  display: flex;
  align-items: flex-end;
  padding: max(env(safe-area-inset-top), 12px) 12px max(env(safe-area-inset-bottom), 12px);
  background: var(--overlay-half);
}

.score-confirm-sheet {
  width: min(100%, 520px);
  max-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 24px);
  overflow-y: auto;
  margin: 0 auto;
  padding: 18px 18px calc(env(safe-area-inset-bottom) + 18px);
  background: var(--panel);
  color: var(--text);
  border-radius: 18px;
}

.score-confirm-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: var(--line-width) solid var(--line);
}

.score-confirm-error {
  min-height: 1.3em;
  color: var(--danger);
}

.score-confirm-actions {
  display: flex;
  gap: 10px;
}

.score-confirm-actions button {
  flex: 1;
  margin-top: 0;
}

/* Survey and shared swipe rows */
.surveybox,
.survey-card,
.harvest-card,
.replant-card,
.screening-card,
.tally-card {
  margin-top: 12px;
  padding: 12px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.rtknet,
.connectivity-banner {
  margin: 10px 0;
  padding: 9px 11px;
  color: var(--warn);
  border: var(--line-width) solid var(--warn);
  border-radius: 8px;
  text-align: center;
}

.svresult {
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-left: 4px solid var(--muted);
  border-radius: 8px;
  font-variant-numeric: tabular-nums;
}

.svresult.green,
.svresult.receiver {
  border-left-color: var(--green);
}

.svresult.amber,
.svresult.acquiring,
.svresult.degraded {
  color: var(--warn);
  border-left-color: var(--warn);
}

.svresult.red,
.svresult.phone,
.svresult.frozen,
.svresult.noisy {
  color: var(--danger);
  border-left-color: var(--danger);
}

.survey-verdict-detail {
  display: block;
  margin-top: 4px;
  color: inherit;
}

.svlog,
.harvest-log,
.replant-log {
  margin-top: 12px;
}

.svitem {
  position: relative;
  overflow: hidden;
  margin-bottom: 6px;
  border-radius: 8px;
  touch-action: pan-y;
}

.svact {
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: 0;
  padding: 0;
  background: var(--danger);
  border: 0;
  border-radius: 8px;
}

.svactlab {
  position: absolute;
  top: 50%;
  left: 0;
  width: 84px;
  color: var(--on-accent);
  font-weight: 700;
  text-align: center;
  white-space: nowrap;
  transform: translate(100vw, -50%);
  transition: none;
  will-change: transform;
}

.svin {
  position: relative;
  z-index: 10;
  padding: 8px 10px;
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-left: 3px solid var(--green);
  border-radius: 8px;
  transition: none;
  will-change: transform;
}

.svitem.open .svin {
  box-shadow: 2px 0 8px var(--shadow-side);
}

.svitem.deleting {
  pointer-events: none;
  transition: height 300ms cubic-bezier(0.2, 0.8, 0.2, 1), margin-bottom 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.svitem.noisy .svin {
  border-left-color: var(--danger);
}

.notemeta,
.bag-meta,
.replant-old {
  display: block;
  margin-top: 2px;
  color: var(--muted);
  font-size: var(--font-body);
  font-variant-numeric: tabular-nums;
}

.svcoord,
.bag-harvest-code,
.replant-new {
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.day-strip {
  margin-top: 10px;
  padding: 8px 10px;
  background: var(--panel-2);
  color: var(--muted);
  border-radius: 8px;
  font-size: var(--font-body);
  text-align: center;
}

.harvest-hold,
.replant-hold,
.screening-alert,
.tally-alert {
  min-height: 1.3em;
  margin-top: 10px;
  padding: 9px 11px;
  border: var(--line-width) solid transparent;
  border-radius: 8px;
}

.harvest-hold.bad,
.replant-hold.bad,
.screening-alert.bad,
.tally-alert.bad {
  color: var(--danger);
  border-color: var(--danger);
}

/* Previous plant and replant forms */
.previous-plants {
  margin-top: 14px;
  border-left: 3px solid var(--line);
  padding-left: 10px;
}

.previous-toggle {
  width: 100%;
  min-height: 48px;
  padding: 8px 0;
  background: transparent;
  color: var(--text);
  border: 0;
  text-align: left;
  font-weight: 700;
}

.previous-body {
  padding-top: 4px;
}

.previous-plant {
  padding: 9px 0;
  border-top: var(--line-width) solid var(--line);
}

.previous-accession,
.previous-name {
  display: block;
  overflow-wrap: anywhere;
}

.previous-generation {
  color: var(--muted);
  font-size: var(--font-body);
}

.old-accession {
  margin-bottom: 12px;
  padding: 10px 12px;
  background: var(--panel-2);
  border-radius: 10px;
}

.new-accession-label,
.new-id-label,
.new-source-label,
.new-note-label {
  margin-top: 10px;
}

.new-accession,
.new-germplasm-id,
.new-source {
  margin-bottom: 4px;
}

.replant-accession-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.accession-check-state {
  grid-column: 1 / -1;
  padding: 8px 10px;
  border: var(--line-width) solid currentColor;
  border-radius: 8px;
  font-weight: 700;
  text-align: center;
}

.accession-check-state.verified {
  color: var(--green);
}

.accession-check-state.not-checked {
  color: var(--danger);
}

.trait-missed {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 10px 0;
  padding: 10px 12px;
  color: var(--danger);
  border: var(--line-width) solid var(--danger);
  border-radius: 8px;
}

.trait-missed button {
  width: auto;
  min-width: 92px;
  margin: 0;
}

.replant-row {
  padding: 8px 0;
  border-top: var(--line-width) solid var(--line);
}

/* Screening */
.screening-criteria {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.screening-criterion {
  padding: 12px;
  background: var(--panel-2);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

.criterion-name {
  margin: 0;
  font-size: 1rem;
}

.criterion-note {
  margin: 5px 0 0;
  color: var(--muted);
  font-size: var(--font-body);
}

.yes-no-controls {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.screen-answer {
  flex: 1;
  min-height: 56px;
  background: var(--panel);
  color: var(--text);
  border-color: var(--line);
  font-size: 1.05rem;
  font-weight: 700;
}

.screen-answer.selected {
  background: var(--green);
  color: var(--on-accent);
  border-color: var(--green-bright);
}

.screen-answer-state {
  margin-top: 7px;
  font-size: var(--font-body);
}

.screen-answer-state.absent {
  color: var(--muted);
}

.screen-answer-state.answered {
  color: var(--success);
  font-weight: 700;
}

.screen-clear {
  min-height: 44px;
  margin-top: 4px;
  padding: 0 10px;
  background: transparent;
  color: var(--muted);
  border: 0;
  text-decoration: underline;
}

/* Tally */
.running-tally {
  padding: 12px;
  background: var(--panel-2);
  border-radius: 10px;
  text-align: center;
}

.tally-count,
.tally-total,
.tally-ground-total {
  font-size: 1.7rem;
  font-variant-numeric: tabular-nums;
}

.tally-count-label,
.tally-total-label,
.tally-ground-label,
.tally-done-state {
  color: var(--muted);
}

.tally-ground-figure {
  white-space: nowrap;
}

.tally-done-state {
  display: block;
  margin-top: 4px;
}

.tally-buttons {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-top: 12px;
}

.tally-button {
  min-height: 64px;
  background: var(--panel-2);
  color: var(--text);
  border-color: var(--line);
  font-size: 1.2rem;
  font-weight: 800;
}

.tally-button.overflow {
  border-color: var(--green-bright);
}

.tally-whole-controls {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin-top: 8px;
}

.tally-typed,
.tally-none {
  width: 100%;
  min-height: var(--tap);
  background: var(--panel);
  color: var(--text);
  border-color: var(--line);
  font-weight: 700;
}

.tally-ground {
  margin-top: 10px;
  padding: 10px;
  background: var(--panel-2);
  border-radius: 10px;
}

.tally-ground-toggle {
  margin: 0;
}

.tally-ground-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  margin-top: 10px;
}

.tally-card input.tally-ground-input {
  min-width: 0;
  width: 100%;
  font-size: 1.2rem;
}

.tally-ground-save {
  width: 100%;
  min-height: var(--tap);
  margin-top: 0;
  padding: 0 12px;
}

.tally-controls {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.tally-undo,
.tally-done {
  flex: 1;
  min-height: var(--tap);
  background: var(--panel);
  color: var(--text);
  border-color: var(--line);
  font-weight: 700;
}

.tally-event-history {
  margin-top: 12px;
}

.tally-event-heading {
  font-size: var(--font-heading-3);
}

.tally-event-list {
  display: flex;
  flex-direction: column;
  margin-top: 6px;
}

.tally-event {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  border-bottom: var(--line-width) solid var(--line);
}

.tally-event-text {
  min-width: 0;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}

.tally-event-remove {
  min-height: 44px;
  padding: 0 10px;
  background: transparent;
  color: var(--muted);
  border-color: var(--line);
  font-weight: 700;
}

.tally-event-more {
  margin-top: 6px;
  color: var(--muted);
  font-size: var(--font-body);
}

/* Field map with sticky axes */
.mapview {
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  margin: 0 0 12px;
}

.maphelp,
.map-too-large {
  margin-bottom: 8px;
  color: var(--muted);
  font-size: var(--font-body);
}

.map-too-large {
  padding: 18px;
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  text-align: center;
}

.mapwrap {
  position: relative;
  width: 100%;
  max-width: 100%;
  max-height: calc(100dvh - 240px);
  overflow: auto;
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

.mapgrid {
  display: grid;
  gap: 3px;
  width: max-content;
  padding: 3px;
  background: var(--bg);
}

.mcell {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  min-width: 44px;
  height: 52px;
  min-height: 44px;
  overflow: hidden;
  padding: 0;
  border-radius: 6px;
  font-size: var(--font-body);
}

.mhead {
  background: var(--bg);
  color: var(--muted);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}

.mtop {
  position: sticky;
  top: 0;
  z-index: 2;
}

.mleft {
  position: sticky;
  left: 0;
  z-index: 2;
}

.mcorner {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 3;
  background: var(--bg);
  color: var(--muted);
}

.mempty {
  background: transparent;
}

.mplot {
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  line-height: 1;
}

.mplot.has {
  background: var(--green);
  color: var(--on-accent);
  border-color: var(--green-bright);
}

.mplot.marked {
  background: var(--marked);
  border-color: var(--marked);
}

.map-marks-summary {
  white-space: pre-line;
}

.mplot.here {
  outline: 3px solid var(--warn);
  outline-offset: -3px;
}

/* Orthomosaic map. The marker canvas never accepts a pointer, so every gesture reaches the viewer. */
.orthomap {
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  border: var(--line-width) solid var(--viewer-control-line);
  border-radius: 10px;
  background: var(--viewer-bg);
}

.orthomap-stage {
  position: relative;
  width: 100%;
  height: min(70dvh, 720px);
  min-height: 420px;
  overflow: hidden;
  background: var(--viewer-bg);
}

.orthomap-viewer,
.orthomap-markers {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.orthomap-markers {
  z-index: 2;
  pointer-events: none;
}

.orthomap-compass {
  position: absolute;
  z-index: 3;
  right: 8px;
  top: 62px; /* under the control bar; the note at the foot can run to three lines and covered it there */
  width: 44px;
  height: 44px;
  border: var(--line-width) solid var(--viewer-control-line);
  border-radius: 50%;
  background: color-mix(in srgb, var(--viewer-panel) 82%, transparent);
  box-shadow: 0 2px 7px var(--shadow);
  cursor: pointer;
  pointer-events: auto;
}

.orthomap-compass-arrow {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  transform-origin: center;
  pointer-events: none;
}

.orthomap-compass-north {
  height: 17px;
  color: var(--accent-label);
  font-size: 1rem;
  font-weight: 800;
  line-height: 1;
}

.orthomap-compass-needle {
  display: block;
  width: 10px;
  height: 23px;
  background: linear-gradient(to bottom, var(--accent-label) 0 50%, var(--viewer-muted) 50% 100%);
  clip-path: polygon(50% 0, 100% 50%, 62% 50%, 50% 100%, 38% 50%, 0 50%);
}

html[data-theme="field"] .orthomap-compass-north {
  color: var(--accent-edge);
}

html[data-theme="field"] .orthomap-compass-needle {
  background: linear-gradient(to bottom, var(--accent-edge) 0 50%, var(--viewer-muted) 50% 100%);
}

.orthomap-controls {
  position: absolute;
  z-index: 4;
  top: 8px;
  left: 8px;
  right: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  pointer-events: none;
}

.orthomap-controls button {
  min-height: 44px;
  padding: 0 12px;
  border: var(--line-width) solid var(--viewer-control-line);
  border-radius: 9px;
  background: var(--viewer-control);
  color: var(--viewer-text);
  font-size: var(--font-body);
  font-weight: 700;
  box-shadow: 0 2px 7px var(--shadow);
  pointer-events: auto;
}

.orthomap-lines,
.orthomap-current,
.orthomap-follow {
  touch-action: manipulation;
}

.orthomap-controls button[aria-pressed="true"] {
  border-color: var(--accent-edge);
  background: var(--accent-fill);
  color: var(--accent-label);
}

.orthomap-status,
.orthomap-callout {
  position: absolute;
  z-index: 4;
  border: var(--line-width) solid var(--viewer-control-line);
  background: var(--viewer-panel);
  color: var(--viewer-text);
  box-shadow: 0 3px 12px var(--shadow);
}

.orthomap-status {
  right: 8px;
  bottom: 8px;
  left: 8px;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: var(--font-body);
  line-height: 1.25;
  text-align: center;
  pointer-events: none;
}

.orthomap-callout {
  width: min(236px, calc(100% - 16px));
  padding: 12px;
  border-radius: 10px;
  font-size: var(--font-body);
}

.orthomap-callout strong {
  display: block;
  overflow-wrap: anywhere;
}

.orthomap-callout-badge,
.orthomap-callout-labels {
  margin-top: 6px;
}

.orthomap-callout-position {
  margin: 8px 0 0;
  color: var(--viewer-muted);
  font-size: var(--font-body);
}

.orthomap-callout-badge.count {
  color: var(--success);
}

.orthomap-callout-badge.skip {
  color: var(--viewer-muted);
}

.orthomap-go {
  width: 100%;
  min-height: 44px;
  margin-top: 10px;
  font-size: var(--font-body);
}

.orthomap-fallback-note {
  margin-bottom: 8px;
  padding: 10px 12px;
  border: var(--line-width) solid var(--warn);
  border-radius: 9px;
  color: var(--text);
  font-size: var(--font-body);
}

.map-imagery-screen {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: min(100%, 620px);
  margin: 0 auto;
  padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
}

.map-imagery-summary,
.map-imagery-state,
.map-imagery-persistence {
  padding: 16px;
  background: var(--panel-2);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.map-imagery-summary h1 {
  margin: 0 0 8px;
  font-size: 1.2rem;
  line-height: 1.25;
}

.map-imagery-summary p,
.map-imagery-captured,
.map-imagery-size,
.map-imagery-state,
.map-imagery-persistence,
.map-imagery-wifi {
  margin: 0;
  font-size: 1rem;
  line-height: 1.45;
}

.map-imagery-state {
  border-left: 4px solid var(--green);
  font-weight: 700;
}

.map-imagery-persistence {
  color: var(--muted);
}

.map-imagery-actions .btn-primary {
  margin-top: 0;
}

.map-imagery-actions .btn-ghost {
  font-size: 1rem;
}

.map-imagery-actions .danger {
  color: var(--danger);
  border-color: var(--danger);
}

.map-imagery-wifi {
  margin-top: 8px;
  color: var(--muted);
}

/* Pedigree panel */
.pedscreen {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
}

.pedscreen .scroll {
  padding-bottom: calc(env(safe-area-inset-bottom) + 24px);
}

.plantfacts {
  margin-bottom: 12px;
}

/* The Plant panel opens on whichever of these three comes first, so none of them carries the gap
   below the top bar any more. Each keeps the space it used to put above itself only where it
   follows something on the same screen. */
.pedwho {
  margin: 0 0 4px;
  font-size: 1.35rem;
  font-weight: 800;
  overflow-wrap: anywhere;
}

.factgrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin: 14px 0 2px;
}

.factgrid > div {
  padding: 9px 11px;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

.factgrid span {
  display: block;
  color: var(--muted);
  font-size: var(--font-body);
}

.plantfacts + .pedwho,
.pedstatus + .pedwho {
  margin-top: 16px;
}

.pedstatus {
  margin: 0 0 2px;
  padding: 9px 11px;
  background: var(--panel-2);
  color: var(--warn);
  border: var(--line-width) solid var(--warn);
  border-radius: 8px;
}

.plantfacts + .pedstatus {
  margin-top: 12px;
}

.pednone {
  margin-top: 10px;
  padding: 14px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

.pedtree,
.pedtree ul,
.pedpair {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pedtree {
  padding: 0 0 4px;
}

.plantfacts + .pedtree,
.pedstatus + .pedtree {
  padding-top: 14px;
}

.pedtree ul,
.pedpair {
  padding-left: 22px;
}

.pedtree ul > li {
  position: relative;
  padding-top: 8px;
}

.pedtree ul > li::before {
  position: absolute;
  top: 26px;
  left: -14px;
  width: 12px;
  height: 2px;
  background: var(--line);
  content: "";
}

.pedtree ul > li::after {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -14px;
  width: 2px;
  background: var(--line);
  content: "";
}

.pedbox {
  position: relative;
  display: block;
  width: 100%;
  margin: 0;
  padding: 9px 12px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  text-align: left;
}

.pedbox.drill {
  padding-right: 32px;
}

/* The drill affordance is the app's own chevron, sat at the right edge of the card; a bare span here
   printed a literal > after every parent name. */
.pedbox.drill .chev {
  position: absolute;
  right: 14px;
  top: 50%;
  margin: -6px 0 0;
}

.pedbox.missing {
  background: transparent;
  border-style: dashed;
}

.pedrole {
  display: block;
  color: var(--muted);
  font-size: var(--font-body);
}

/* Gallery and deliberate dark viewer */
.gallery-list {
  display: flex;
  flex-direction: column;
  gap: var(--list-gap);
}

/* THE SCOPE OF THE LIST STAYS ON SCREEN WHILE THE LIST SCROLLS UNDER IT. The sentence explaining
   whether the gallery held one trial's photos or every photo on the phone scrolled away on the first
   flick. It paints on a surface token because rows
   pass beneath it. */
/* THE CONTROL THAT DRAWS THE NEXT PAGE OF THE GALLERY. The list is paged because drawing 1,700 rows
   at once froze the whole app on 2026-09-07, so this is how the rest are reached. It sits under the
   list, full width, and reads as an ordinary action rather than as a warning. */
.gallery-more {
  display: block;
  width: 100%;
  margin: 12px 0 0;
}

.gallery-summary {
  position: sticky;
  top: 0;
  z-index: 3;
  margin-bottom: 8px;
  padding: 8px 0;
  background: var(--bg);
}

#gallerySync {
  width: 100%;
  margin-bottom: 12px;
}

.gallery-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 10px 12px;
  background: var(--panel);
  color: var(--text);
  border-color: var(--line);
  text-align: left;
}

.gallery-row small {
  display: block;
  color: var(--muted);
}

.gallery-thumb {
  display: flex;
  flex: 0 0 54px;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  overflow: hidden;
  background: var(--panel-2);
  border-radius: 8px;
}

.gallery-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.gallery-copy {
  flex: 1;
  min-width: 0;
}

/* THE ROW LEADS WITH THE PLANT AND WRAPS. It used to lead with the raw filename on one clipped
   line, so the identity a person actually says out loud was the part that got cut off. */
.gallery-name {
  display: block;
  min-width: 0;
  font-weight: 700;
  overflow-wrap: anywhere;
  white-space: normal;
}

.gallery-row small,
.gallery-file {
  overflow-wrap: anywhere;
  white-space: normal;
}

.viewer {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  flex-direction: column;
  background: var(--viewer-bg);
  color: var(--viewer-text);
}

.viewer-top {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: calc(env(safe-area-inset-top) + 8px) 12px 8px;
  background: var(--viewer-panel);
  color: var(--viewer-text);
}

/* THE TOP BAR CARRIES A POSITION COUNTER AND NOTHING ELSE BETWEEN ITS TWO CONTROLS. It used to
   carry the filename on one line with text-overflow: ellipsis, so on every phone the identity ended
   in "..."; the identity now lives in the foot, where it wraps and nothing is cut off. */
.viewer-count {
  flex: 1;
  text-align: center;
}

.viewer-top button {
  min-height: 44px;
  padding: 0 10px;
  background: var(--viewer-control);
  color: var(--viewer-text);
  border: 0;
}

.viewer-img {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  overflow: auto;
}

.viewer-img img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* THREE LINES, EACH WRAPPING, NONE TRUNCATED: the plant, then where and when the photo was taken
   with its filename, then what the phone is doing about it. */
.viewer-foot {
  display: flex;
  flex: none;
  flex-direction: column;
  gap: 4px;
  padding: 8px 12px calc(env(safe-area-inset-bottom) + 8px);
  background: var(--viewer-panel);
  color: var(--viewer-muted);
  text-align: left;
}

.viewer-line {
  overflow-wrap: anywhere;
  white-space: normal;
}

.viewer-foot .viewer-line:first-child {
  color: var(--viewer-text);
  font-weight: 700;
}

/* PREVIOUS AND NEXT ARE ALWAYS DRAWN. A swipe with no visible affordance is not discoverable, and
   the crew wear gloves. */
.viewer-nav {
  display: flex;
  flex: none;
  gap: 8px;
  padding: 8px 12px;
  background: var(--viewer-panel);
}

.viewer-step {
  flex: 1;
  min-height: 44px;
  background: var(--viewer-control);
  color: var(--viewer-text);
  border: 0;
}

.viewer-step:disabled {
  opacity: 0.4;
}

/* A photo whose local original has gone shows its stored thumbnail scaled up rather than nothing at
   all, and it must not be blown up beyond what a 320 px thumbnail can carry. */
.viewer-img-scaled img {
  width: auto;
  max-width: min(100%, 640px);
  image-rendering: auto;
}

.viewer-img-missing::before {
  color: var(--viewer-muted);
  content: "No picture is stored on this phone";
}

/* Full screen scanner and manual entry */
.scanscreen {
  position: fixed;
  inset: 0;
  z-index: 75;
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  background: var(--scanner-bg);
  color: var(--viewer-text);
}

.scanscreen .topbar {
  position: relative;
  flex: none;
  background: var(--viewer-panel);
  color: var(--viewer-text);
  border-color: var(--viewer-line);
}

.scanscreen .topbar button,
.scanfoot button {
  min-height: 44px;
  background: var(--viewer-control);
  color: var(--viewer-text);
  border-color: var(--viewer-control-line);
}

.scanwrap {
  position: relative;
  flex: 1;
  min-height: 300px;
  overflow: hidden;
  background: var(--scanner-bg);
}

.scanwrap video,
.scanfreeze {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.scanfreeze {
  display: none;
}

.scanbox {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(62vw, 260px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border: 4px solid var(--scanner-frame);
  border-radius: 18px;
  box-shadow: 0 0 0 9999px var(--scanner-mask);
  pointer-events: none;
}

.scanid {
  position: absolute;
  top: 50%;
  left: 50%;
  display: none;
  max-width: 86%;
  padding: 10px 18px;
  transform: translate(-50%, -50%);
  background: var(--media-overlay);
  color: var(--media-text);
  border: 2px solid var(--scanner-frame);
  border-radius: 12px;
  font-size: 1.6rem;
  font-weight: 700;
  text-align: center;
  overflow-wrap: anywhere;
}

.scantip {
  position: absolute;
  right: 0;
  bottom: 14px;
  left: 0;
  padding: 0 18px;
  /* Camera brightness varies, so the tip needs its own contrasting backdrop. */
  background: var(--media-overlay);
  color: var(--media-text);
  text-align: center;
  text-shadow: 0 1px 3px var(--scanner-shadow);
}

.scanfoot {
  flex: none;
  padding: 12px 14px calc(env(safe-area-inset-bottom) + 12px);
  background: var(--viewer-panel);
  color: var(--viewer-text);
}

.scanhit {
  min-height: 0;
  color: var(--viewer-text);
}

.scanhit.ok {
  padding-left: 10px;
  border-left: 4px solid var(--green-bright);
}

.scanhit.miss {
  padding-left: 10px;
  border-left: 4px solid var(--scanner-miss);
}

.scanactions:empty {
  display: none;
}

.scanactions .btn-go {
  margin-top: 10px;
  background: var(--accent-fill);
  color: var(--accent-label);
  border-color: var(--accent-edge);
}

.manualtag {
  position: fixed;
  inset: 0;
  z-index: 76;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(env(safe-area-inset-top), 14px) 14px max(env(safe-area-inset-bottom), 14px);
  background: var(--overlay-strong);
}

.manualtag-panel {
  width: min(100%, 460px);
  padding: 18px;
  background: var(--panel);
  color: var(--text);
  border-radius: var(--radius);
}

.manualtag-panel button {
  min-width: 96px;
  min-height: var(--tap);
  margin: 12px 8px 0 0;
  background: var(--panel-2);
  color: var(--text);
  border-color: var(--line);
}

.manualtag-panel .btn-go {
  background: var(--accent-fill);
  color: var(--accent-label);
  border-color: var(--accent-edge);
}

.findscreen {
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
}

.find-content {
  height: calc(100% - 64px);
}

.find-position {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  align-items: end;
  gap: 8px;
}

/* Row and Plant are the first thing on the Find screen, and the shared label rule's own top margin
   would sit on top of the screen gap. The container owns that distance here as everywhere else. */
.find-position > label {
  margin-top: 0;
}

.find-position .btn-go {
  min-width: 72px;
  min-height: var(--tap);
}

.find-message {
  min-height: 1.5em;
  margin: 8px 0;
  color: var(--danger);
  font-weight: 700;
}

.find-search-label {
  display: block;
}

.find-result {
  width: 100%;
  text-align: left;
}

.find-more { color: var(--muted); padding: 12px 16px; }
.find-photo-count {
  color: var(--muted);
  white-space: nowrap;
}

/* Definition dialog is mounted by the application shell, outside the UI module tree. */
.definition-overlay {
  position: fixed;
  inset: 0;
  z-index: 65;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(env(safe-area-inset-top), 14px) 14px max(env(safe-area-inset-bottom), 14px);
  background: var(--overlay-strong);
}

.definition-panel {
  width: min(100%, 460px);
  max-height: 100%;
  overflow-y: auto;
  padding: 18px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.definition-panel button {
  width: 100%;
  min-height: var(--tap);
  padding: 0 14px;
  background: var(--panel-2);
  color: var(--text);
  border-color: var(--line);
}

/* The status row keeps its label and chevron apart without adding a second card border. */
.receiver-corrections-link {
  justify-content: space-between;
  border: 0;
  min-height: var(--tap);
}

/* Receiver status */
.receiver-screen,
.corrections-screen {
  gap: 12px;
  max-width: 760px;
  width: 100%;
  margin: 0 auto;
}

/* The heading opens the Receiver screen, so its distance below the top bar comes from the
   scrolling container like every other screen's. */
.receiver-title {
  margin: 0;
}

.receiver-connection,
.receiver-verification,
.receiver-truth,
.receiver-corrections,
.receiver-failure,
.receiver-accuracy,
.receiver-satellites,
.receiver-position,
.receiver-mark-check,
.receiver-mark-result,
.receiver-tested {
  padding: 12px;
  background: var(--panel);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

.receiver-verification-title,
.receiver-mark-confirmation-text {
  margin: 0 0 7px;
  color: var(--text);
  font-size: var(--font-body);
}

.receiver-verification-state {
  margin-bottom: 8px;
  color: var(--warn);
  font-size: var(--font-body);
  font-weight: 800;
}

.receiver-verification-state.success {
  color: var(--success);
}

.receiver-verification-note {
  margin-bottom: 8px;
  color: var(--muted);
  font-size: var(--font-body);
  line-height: var(--line-body);
}

.receiver-connection-state,
.receiver-solution {
  font-size: var(--font-body);
}

.status-chip {
  display: inline-flex;
  align-items: center;
  width: fit-content;
  min-height: 34px;
  padding: 4px 12px;
  background: var(--panel-2);
  border: var(--line-width) solid var(--line);
  border-radius: 999px;
  font-size: var(--font-body);
  font-weight: 800;
  line-height: 1.2;
}

.receiver-solution-chip {
  flex: none;
  max-width: none;
  min-width: max-content;
  color: var(--muted);
  appearance: none;
  white-space: nowrap;
}

.receiver-solution-chip.success {
  color: var(--success);
}

.receiver-solution-chip.warning {
  color: var(--warn);
}

.receiver-solution-chip.neutral {
  color: var(--muted);
}

.receiver-connection-detail,
.receiver-solution-detail,
.receiver-device,
.receiver-baseline,
.receiver-epoch,
.receiver-mark-detail,
.receiver-mark-status,
.correction-tick {
  margin-top: 5px;
  color: var(--muted);
  font-size: var(--font-body);
}

/* The tested-receiver list is read, not operated, and it answers one question: has this model been
   measured, and against what. It is a four-column table for that reason. The model column takes
   whatever width it needs and wraps; the other three are as narrow as their contents, so the table
   fits a phone without a horizontal scroller. */
.receiver-tested-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: auto;
}

.receiver-tested-cell {
  padding: 6px 8px 6px 0;
  color: var(--text);
  font-size: var(--font-body);
  text-align: left;
  vertical-align: top;
  /* break-word, not anywhere. "anywhere" breaks inside ordinary words whenever a column is narrow,
     which rendered the status column as "Verifie" over "d in the field" on a phone. break-word only
     breaks a word that cannot fit a line by itself, which is the case this needed to cover. */
  overflow-wrap: break-word;
}

.receiver-tested-head .receiver-tested-cell {
  color: var(--muted);
  font-weight: var(--font-weight-heading);
  white-space: nowrap;
}

.receiver-tested-body .receiver-tested-row {
  border-top: var(--line-width) solid var(--line);
}

.receiver-accuracy-label,
.receiver-corrections-title,
.receiver-position-label,
.receiver-mark-title,
.receiver-mark-result-title,
.receiver-tested-title {
  margin: 0 0 7px;
  color: var(--text);
  font-size: var(--font-body);
}

.receiver-accuracy-east,
.receiver-accuracy-north,
.receiver-accuracy-up,
.receiver-sat-rover,
.receiver-sat-base,
.receiver-sat-valid,
.receiver-position-latitude,
.receiver-position-longitude,
.receiver-position-height,
.receiver-mark-mean-latitude,
.receiver-mark-mean-longitude,
.receiver-mark-mean-height,
.receiver-mark-count,
.receiver-mark-rms {
  font-variant-numeric: tabular-nums;
}

.receiver-mark-check {
  display: grid;
  gap: 8px;
}

.receiver-mark-results {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 8px;
}

.receiver-mark-result {
  background: var(--panel-2);
}

.receiver-mark-confirmation {
  padding: 12px;
  background: var(--panel-2);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

/* Confirm sits directly under a paragraph explaining what confirming means, and with only the
   default gap the button read as part of that paragraph rather than as the decision it commits.
   A survey verification is not an easy thing to undo, so the control that commits it gets room. */
.receiver-mark-confirmation-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 20px;
}

.receiver-mark-confirmation-actions button {
  flex: 1 1 180px;
  min-height: 54px;
}

.receiver-mark-check .btn-primary {
  width: 100%;
  margin-top: 0;
}

/* The row carries its own name first and its status second, so the name is the weighted one and the
   status reads as a subtitle. Before this the status was the only text in the row, which left a
   tappable card whose whole label was "Receiver counters unchanged". */
.receiver-corrections-name {
  font-weight: 700;
  margin-right: auto;
}

.receiver-correction-state {
  font-weight: 400;
  opacity: 0.8;
  margin-right: 8px;
}

.receiver-correction-state.success {
  font-weight: 700;
  opacity: 1;
}

.receiver-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.receiver-chooser {
  display: grid;
  gap: var(--space-1);
}

.receiver-chooser button {
  min-width: 0;
  min-height: 48px;
  overflow-wrap: anywhere;
  text-align: left;
}

.receiver-controls .btn-primary {
  flex: 1 1 180px;
  margin-top: 0;
}

.receiver-error {
  margin-top: 0;
}

.success,
.green {
  color: var(--green);
}

.warning,
.amber {
  color: var(--warn);
}

.danger,
.red,
.bad {
  color: var(--danger);
}

.receiver-epoch.frozen-stream {
  padding: 8px;
  background: var(--frozen-bg);
  color: var(--danger);
  border: var(--line-width) solid var(--danger);
  border-radius: 8px;
}

/* Recorded value wash. The show class controls entry and the default state fades out. */
.okflash {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--flash-success);
  color: var(--flash-text);
  font-size: 34vw;
  font-weight: 800;
  line-height: 1;
  padding: max(env(safe-area-inset-top), 12px) 12px max(env(safe-area-inset-bottom), 12px);
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease-out;
}

.okflash.show {
  opacity: 1;
  transition: none;
}

.okflash.bad {
  background: var(--flash-danger);
  color: var(--media-text);
  font-size: 12vw;
}

/* Every tappable component uses the mode-row pressed treatment. */
.btn-primary,
.btn-go,
.btn-ghost,
.iconbtn,
.chip,
.pedchip,
.row,
.category-row,
.trait-choice,
.modebtn,
.capture,
.notesave,
.rb,
.ratetick,
.disclosure-row,
.notesect,
.previous-toggle,
.screen-answer,
.screen-clear,
.tally-button,
.tally-typed,
.tally-none,
.tally-ground-save,
.tally-event-remove,
.tally-undo,
.tally-done,
.mplot,
.orthomap-compass,
.pedbox.drill,
.gallery-row,
.find-result,
.syncstrip,
.thumbs .thumb,
.svact,
.receiver-solution-chip,
.menu-item,
.multi-arrow,
.multi-dot,
.topbar-back,
.topbar-menu,
.noteact,
.replant-scan,
.replant-check,
.navrow button,
.score-confirm-actions button,
.pedscreen .topbar button,
.viewer-top button,
.scanscreen .topbar button,
.scanfoot button,
.manualtag-panel button,
.definition-panel button,
.settings-menu-sheet .menu-item,
.sign-out-overlay .menu-item,
.manual-nav-button,
.bottom-bar button {
  /* The separator hairline is positioned against this. */
  position: relative;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: var(--pressed-transition);
}

.btn-primary:active:not(:disabled),
.btn-go:active:not(:disabled),
.btn-ghost:active:not(:disabled),
.iconbtn:active:not(:disabled),
.chip:active:not(:disabled),
.pedchip:active:not(:disabled),
.row:active:not(:disabled),
.category-row:active:not(:disabled),
.trait-choice:active:not(:disabled),
.modebtn:active:not(:disabled),
.capture:active:not(:disabled),
.notesave:active:not(:disabled),
.rb:active:not(:disabled),
.ratetick:active:not(:disabled),
.disclosure-row:active:not(:disabled),
.notesect:active:not(:disabled),
.previous-toggle:active:not(:disabled),
.screen-answer:active:not(:disabled),
.screen-clear:active:not(:disabled),
.tally-button:active:not(:disabled),
.tally-typed:active:not(:disabled),
.tally-none:active:not(:disabled),
.tally-ground-save:active:not(:disabled),
.tally-event-remove:active:not(:disabled),
.tally-undo:active:not(:disabled),
.tally-done:active:not(:disabled),
.mplot:active:not(:disabled),
.orthomap-compass:active:not(:disabled),
.orthomap-controls button:active:not(:disabled),
.pedbox.drill:active:not(:disabled),
.gallery-row:active:not(:disabled),
.find-result:active:not(:disabled),
.syncstrip:active:not(:disabled),
.thumbs .thumb:active:not(:disabled),
.svact:active:not(:disabled),
.receiver-solution-chip:active:not(:disabled),
.menu-item:active:not(:disabled),
.multi-arrow:active:not(:disabled),
.multi-dot:active:not(:disabled),
.topbar-back:active:not(:disabled),
.topbar-menu:active:not(:disabled),
.noteact:active:not(:disabled),
.replant-scan:active:not(:disabled),
.replant-check:active:not(:disabled),
.navrow button:active:not(:disabled),
.score-confirm-actions button:active:not(:disabled),
.pedscreen .topbar button:active:not(:disabled),
.viewer-top button:active:not(:disabled),
.scanscreen .topbar button:active:not(:disabled),
.scanfoot button:active:not(:disabled),
.manualtag-panel button:active:not(:disabled),
.definition-panel button:active:not(:disabled),
.settings-menu-sheet .menu-item:active:not(:disabled),
.sign-out-overlay .menu-item:active:not(:disabled),
.manual-nav-button:active,
.bottom-bar button:active:not(:disabled) {
  background: var(--pressed-tint);
  color: var(--text);
  transform: scale(var(--pressed-scale));
  transition: none;
}

/* Full screen and in-button loading treatments. */
.loading-screen {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  gap: 12px;
  padding: max(env(safe-area-inset-top), 24px) 24px max(env(safe-area-inset-bottom), 24px);
  background: var(--bg);
  color: var(--text);
  text-align: center;
}

.loading-screen .loading-topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  text-align: left;
}

.loading-label {
  color: var(--muted);
  font-weight: 600;
}

.spinner {
  width: 44px;
  height: 44px;
  border: 4px solid var(--line);
  border-top-color: var(--green);
  border-radius: 50%;
  animation: spinner-turn 700ms linear infinite;
}

.btn-busy {
  position: relative;
  padding-right: 42px;
  opacity: 0.62;
  pointer-events: none;
}

.btn-busy::after {
  position: absolute;
  top: 50%;
  right: 14px;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  content: "";
  animation: spinner-turn 700ms linear infinite;
}

@keyframes spinner-turn {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .spinner,
  .btn-busy::after {
    animation: none;
    border-right-color: currentColor;
  }

  /* A person who has asked for less motion gets the sheet and its scrim at once, in both
     directions. menu.js reads the same preference and removes a closed overlay immediately rather
     than waiting for a transition that will never run. */
  .menu-overlay,
  .menu-sheet {
    transition: none;
  }
}

/* The field manual stays inside the standalone app with app-owned navigation. */
.manual-viewer {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--bg);
}

/* The welcome tour keeps horizontal movement in the browser's native scroller. Buttons only move
   to the next snap point; touch and trackpad gestures need no JavaScript translation. */
.tour-screen {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
  padding-bottom: max(env(safe-area-inset-bottom), 12px);
  background: var(--bg);
}

.tour-track {
  display: flex;
  flex: 1;
  min-height: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.tour-track::-webkit-scrollbar {
  display: none;
}

.tour-slide {
  display: flex;
  flex: 0 0 100%;
  flex-direction: column;
  min-width: 0;
  overflow-y: auto;
  padding: max(env(safe-area-inset-top), 16px) 18px 8px;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.tour-image {
  display: block;
  width: min(100%, 480px);
  height: min(51dvh, 520px);
  object-fit: contain;
  margin: 0 auto;
  border: var(--line-width) solid var(--line);
  border-radius: 18px;
  box-shadow: 0 3px 14px var(--shadow-soft);
}

.tour-copy {
  width: min(100%, 540px);
  margin: 18px auto 0;
  text-align: center;
}

.tour-title {
  color: var(--green);
}

.tour-body {
  margin-top: 10px;
  color: var(--text);
  line-height: var(--line-body);
}

.tour-actions {
  display: flex;
  gap: 12px;
  width: min(100%, 540px);
  margin: auto auto 0;
  padding-top: 18px;
}

.tour-actions button {
  flex: 1;
  margin: 0;
}

.tour-skip,
.tour-next {
  min-width: 0;
}

.tour-dots {
  display: flex;
  flex: none;
  justify-content: center;
  gap: 9px;
  min-height: 24px;
  padding: 8px 18px 0;
}

.tour-dot {
  width: 9px;
  height: 9px;
  background: var(--line);
  border-radius: 50%;
}

.tour-dot.current {
  background: var(--green);
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--green);
}

@media (max-height: 640px) {
  .tour-image { height: 37dvh; }
  .tour-copy { margin-top: 12px; }
  .tour-actions { padding-top: 12px; }
}

.manual-toolbar {
  flex: 0 0 auto;
}

.manual-back {
  flex: none;
}

.manual-close {
  flex: none;
  width: auto;
  margin-left: auto;
  padding: 0 12px;
}

.manual-body {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  background: var(--bg);
}

.manual-home,
.manual-section-content {
  width: 100%;
  max-width: 760px;
  margin: 0 auto;
}

/* ONE GAP BETWEEN EVERYTHING ON THE HELP HOME: the search field, the section rows, and the Browse
   sections heading a fruitless search puts above them. The heading keeps its side margin, which
   lines its text up with a row's text, and gives up its own vertical margin to the list gap. */
.manual-search {
  margin-bottom: var(--list-gap);
}

.manual-results,
.manual-list {
  display: flex;
  flex-direction: column;
  gap: var(--list-gap);
}

.manual-search-empty {
  min-height: 180px;
  padding: 24px 12px;
}

.manual-list-heading {
  margin: 0 2px;
}

.manual-section-content {
  padding: 18px;
  overflow-wrap: anywhere;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.manual-section-content h2,
.manual-section-content h3 {
  scroll-margin-top: 16px;
}

.manual-section-content h3 {
  margin-top: 24px;
}

.manual-section-content p,
.manual-section-content ol,
.manual-section-content ul,
.manual-section-content .figs,
.manual-section-content .fig,
.manual-section-content .fright {
  margin-top: 12px;
}

.manual-section-content img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  margin: 0 auto;
  border-radius: 8px;
}

.manual-section-content .figs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: 16px;
}

.manual-section-content .cap {
  margin-top: 6px;
  color: var(--muted);
  font-size: var(--font-body);
  line-height: var(--line-body);
}

.manual-nav {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-top: 24px;
  padding-top: 16px;
  border-top: var(--line-width) solid var(--line);
}

.manual-nav-button {
  min-width: 0;
  margin-top: 0;
  text-align: center;
}

.manual-nav-button:only-child {
  grid-column: 1 / -1;
}

/* Wave two shared field context. These components did not exist in the TreeScribe shell before this port. */
.cardrow {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.cardrow > button {
  margin-top: 8px;
}

.fixwarn {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--danger);
  border-color: var(--danger);
  text-align: left;
}

.warnbtn {
  color: var(--danger);
  border-color: var(--danger);
  font-weight: 800;
}

.warnx {
  flex: 0 0 auto;
  width: auto;
  margin: 0 0 0 auto;
}

.disclosure-section {
  margin-top: 12px;
}

.notesect {
  margin: 0;
  overflow: hidden;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
}

.notehead {
  justify-content: space-between;
  width: 100%;
  margin: 0;
  text-align: left;
}

.notesect[aria-expanded="true"] {
  border-radius: 10px 10px 0 0;
}

.notebody,
.priorbody {
  padding: 12px;
  overflow: hidden;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-top: 0;
  border-radius: 0 0 10px 10px;
}

.notelabel {
  display: block;
  margin-bottom: 6px;
  color: var(--muted);
  font-size: var(--font-body);
}

.notelog {
  margin-top: 12px;
}

.secthead {
  margin-bottom: 8px;
  color: var(--muted);
  font-size: var(--font-body);
  font-weight: 700;
}

.noteitem {
  padding: 10px 0;
  border-top: var(--line-width) solid var(--line);
}

.noteitem:first-child {
  border-top: 0;
}

.notetext {
  margin-top: 4px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.noteacts {
  display: flex;
  justify-content: flex-end;
  margin-top: 6px;
}

.noteact {
  width: auto;
  min-width: 88px;
  margin: 0;
}

.prevsect {
  border-color: var(--warn);
}

.prevsect + .priorbody {
  border-color: var(--warn);
}

.prevplant {
  padding: 12px 0;
  border-top: var(--line-width) solid var(--line);
}

.prevplant:first-child {
  padding-top: 0;
  border-top: 0;
}

.prevhead {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 10px;
}

.prevtag,
.prevmeta {
  color: var(--muted);
  font-size: var(--font-body);
}

.prevwhy {
  margin: 6px 0;
  color: var(--danger);
  font-weight: 700;
}

.pednote {
  display: block;
  color: var(--muted);
  font-size: var(--font-body);
}

.pedmeta {
  margin-top: 12px;
  padding: 10px 12px;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  line-height: 1.5;
}

/* TEMPORARY: THE DIAGNOSTICS SHEET. This block belongs to public/app/ui/diagnostics.js, a screen
   built for the phone-testing phase that comes out before the crew's cutover; deleting that module,
   the Settings entry that opens it, and this block is the whole removal. The report is a readonly
   textarea rather than a paragraph so that a phone with no clipboard permission can still hand the
   whole text over as a selection, and its lines are left unwrapped so a tap log reads as a column. */
.diagnostics-report {
  width: 100%;
  min-height: 220px;
  max-height: 45dvh;
  margin: 10px 0;
  padding: 10px;
  background: var(--panel-2);
  color: var(--text);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--font-body);
  line-height: 1.4;
  white-space: pre;
  overflow: auto;
  resize: none;
}

/* The one action that removes a trial's records sits in its own group on that sheet, so the report
   above it and the removal below it cannot be mistaken for one control. It leaves with the rest of
   this block. */
.menu-section-trial-reset {
  margin-top: 4px;
}

/* The campaign seeder and its remover sit in a group of their own on the same sheet, for the same
   reason: two dangerous actions and a count chooser must not read as one control. The chooser is
   given the same height as a row so a thumb finds it where it expects a row. It leaves with the
   rest of this block. */
.menu-section-campaign-seed {
  margin-top: 4px;
}

.menu-section-campaign-seed select {
  width: 100%;
  min-height: 44px;
  margin: 4px 0;
  padding: 0 12px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--font-body);
}

.orthomap-note { margin: 0; }
.orthomap-counted { margin-top: 4px; }
.orthomap-counted-entries { overflow-wrap: anywhere; }
.orthomap-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.orthomap-legend span { display: inline-flex; align-items: center; gap: 4px; }
.orthomap-legend i { width: 10px; height: 10px; border: 1px solid rgba(0,0,0,0.72); border-radius: 50%; }
.tally-counted-entries { margin-bottom: 8px; }

/* Caster settings stay in one column so long mountpoint names fit a phone in the field. */
.receiver-correction-source {
  padding: 12px;
  background: var(--panel);
  border: var(--line-width) solid var(--line);
  border-radius: 10px;
  overflow-wrap: anywhere;
}
.receiver-correction-source summary { font-weight: 600; cursor: pointer; }
.ntrip-write-target { display: grid; gap: 6px; min-width: 0; }
.ntrip-write-target select { width: 100%; min-width: 0; box-sizing: border-box; }
.ntrip-settings { display: grid; gap: 8px; }
.ntrip-settings label { display: grid; gap: 4px; margin: 0; }
.ntrip-settings input:not([type="checkbox"]), #ntripSearch { width: 100%; min-width: 0; box-sizing: border-box; }
.ntrip-mountpoints { display: grid; gap: 8px; max-height: 280px; overflow: auto; }
.ntrip-mountpoints button { text-align: left; white-space: normal; overflow-wrap: anywhere; }
.ntrip-status { font-variant-numeric: tabular-nums; }

/* Pairing controls must leave the camera view fully visible. */
.lab-scanner[hidden] { display: none; }

/* A SIX CHARACTER CODE DESERVES SIX CHARACTERS' WORTH OF ROOM, and under 16px iOS zooms the page in
   when the field takes focus and will not zoom back out, which is what made this unusable rather than
   merely small. The code is read aloud off a screen across the room and typed once, so it is set
   large, spaced and centered, the way the desktop displays it. */
.lab-scanner input {
  width: 100%;
  box-sizing: border-box;
  text-transform: uppercase;
  font-size: 2rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.35em;
  text-indent: 0.35em;
  text-align: center;
  min-height: 64px;
  padding: 8px 10px;
}

.lab-scanner #labNotice { overflow-wrap: anywhere; }

/* Larger type and compact spacing keep receiver actions readable within a phone viewport. */
.receiver-screen,
.corrections-screen { --font-body: 1.125rem; font-size: var(--font-body); }
.receiver-screen > .receiver-controls button { flex: 1; min-height: 52px; margin: 0; }
.receiver-correction-source p { margin: 8px 0; }
.receiver-correction-source summary { min-height: 44px; align-content: center; }
.ntrip-settings input:not([type="checkbox"]),
.ntrip-write-target select,
#ntripSearch { min-height: 48px; font-size: var(--font-body); margin: 0; }
.ntrip-settings .ntrip-checkbox {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  cursor: pointer;
}
.ntrip-checkbox input[type="checkbox"] {
  flex: 0 0 24px;
  width: 24px;
  height: 24px;
  margin: 0;
  accent-color: var(--green);
}
.receiver-correction-source .receiver-controls button { flex: 1 1 100px; min-height: 48px; margin: 0; }
#ntripStart[aria-pressed="true"] { background: var(--green); color: var(--bg); opacity: 1; }
#ntripStop[aria-pressed="true"] { background: var(--panel-2); border-color: var(--muted); color: var(--text); opacity: 1; }

/* THE LAB'S SIDE-BY-SIDE ROWS. Without this the row holding a heading and its button had no
   alignment at all, so "Change trial" stood taller than the trial name it sat beside and read as the
   more important of the two. The heading takes the space and the button takes only what it needs. */
.lab-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.lab-row > h2 {
  margin: 0;
  margin-right: auto;
}

.lab-row > button {
  width: auto;
  flex: 0 0 auto;
  margin-top: 0;
  padding: 6px 14px;
  min-height: var(--tap);
}

/* Confirming a phone is a real decision, so it is the accent control rather than another ghost
   button, but it is one short answer and does not need the full width btn-go gives a form's submit. */
.lab-confirm {
  width: auto;
  align-self: flex-start;
  padding: 8px 18px;
}

/* THE LAB PAGE HAD NO VERTICAL RHYTHM AT ALL. .lab-layout carried no styling, so its sections butted
   against each other and a rule sat hard on the buttons below it with no gap, which reads as a
   rendering fault rather than a boundary. */
.lab-layout {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.lab-layout > section {
  padding-top: 4px;
}

.lab-layout > section + section {
  border-top: 1px solid var(--line);
  padding-top: 28px;
}

/* THE ASSISTANT SCREEN. A page somebody stands in a field holding at arm's length, so the answer is
   the largest thing on it and the controls are thumb-sized. Everything takes its color from the
   existing tokens, so it follows the light, dark and field themes without a second palette.
   NOTHING HERE SHRINKS BELOW THE BODY SIZE. This app is read at arm's length in sunlight, so the
   stylesheet carries no type under 1rem anywhere and de-emphasis is done with color instead. Six
   small sizes were written here and the type guard refused all six. */
.assistant-screen {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.assistant-scope {
  color: var(--muted);
  font-size: var(--font-body);
  margin: 0;
}

.assistant-ask {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Press and hold, so it is sized for a gloved thumb rather than a cursor. */
.assistant-talk {
  min-height: 72px;
  font-size: 1.15rem;
  touch-action: none;
  user-select: none;
}

.assistant-talk.is-listening {
  background: var(--accent-edge);
  border-color: var(--accent-edge);
}

.assistant-hint {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
  min-height: 1.3em;
}

.assistant-status {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
}

.assistant-status.is-error {
  color: var(--warn);
}

.assistant-log {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Newest first, because the answer just spoken is the one being read. */
.assistant-exchange {
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-2);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.assistant-exchange.is-failed {
  border-color: var(--warn);
}

.assistant-asked {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
}

/* The answer carries the weight on the screen, since it is what was asked for. */
.assistant-answer {
  margin: 0;
  font-size: 1.05rem;
  line-height: 1.45;
}

.assistant-spoken {
  color: var(--muted);
  font-size: var(--font-body);
}

.assistant-suggestions {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-1);
}

.assistant-suggestions-title {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.assistant-suggestion {
  font: inherit;
  text-align: left;
  color: var(--text);
  background: var(--panel-2);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  min-height: var(--tap);
  cursor: pointer;
}

.assistant-suggestion:active:not(:disabled) {
  background: var(--accent-fill);
  color: var(--accent-label);
  border-color: var(--accent-edge);
}

/* THE VOICE RECORDING SCREEN. One button and a list, because the only two things a person does here
   are start talking and check that what they said is still somewhere. */
.voice-screen {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.voice-scope {
  color: var(--muted);
  font-size: var(--font-body);
  margin: 0;
}

/* THE LIMITATION IS NOT SMALL PRINT. It is the same size as everything else and it sits above the
   button, because a person who pockets the phone and keeps talking has lost the walk, and a warning
   they did not read is a warning that was not given. */
.voice-limit {
  margin: 0;
  font-size: var(--font-body);
  border-left: 4px solid var(--warn);
  padding: var(--space-2);
  background: var(--panel-2);
}

.voice-control {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* Sized for a gloved thumb, the same as the assistant's. */
.voice-toggle {
  min-height: 72px;
  font-size: 1.15rem;
  user-select: none;
}

.voice-toggle.is-recording {
  background: var(--warn);
  border-color: var(--warn);
}

/* THE CLOCK IS THE ONLY PROOF THE MICROPHONE IS ALIVE, so it holds its line whether or not it is
   running; a figure that appears and collapses the layout reads as a glitch rather than a state. */
.voice-elapsed {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
  font-variant-numeric: tabular-nums;
  min-height: 1.3em;
}

.voice-blocked,
.voice-status {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
}

.voice-status.is-error {
  color: var(--warn);
}

.voice-listhead {
  margin: var(--space-2) 0 0;
  font-size: var(--font-body);
}

.voice-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.voice-empty {
  margin: 0;
  color: var(--muted);
  font-size: var(--font-body);
}

.voicerow {
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-2);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* A recording that exists on this phone alone is bordered, not labeled quietly, because it is the
   one state where closing the app in the wrong place loses an hour of somebody's work. */
.voicerow.is-pending,
.voicerow.is-warn {
  border-color: var(--warn);
}

.voicerow.is-live {
  border-color: var(--accent-edge);
}

.voicerow-head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-1);
  font-variant-numeric: tabular-nums;
}

.voicerow-state,
.voicerow-who {
  margin: 0;
  font-size: var(--font-body);
}

.voicerow-who {
  color: var(--muted);
}
