
.fld-mapwrap {
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-xs);
  margin-bottom: var(--tn-space-lg);
}

.fld-map {
  position: relative;
  height: 52vh;
  height: min(52dvh, 420px);
  min-height: 280px;
  background: var(--tn-color-surface-sunken);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-xl);
  box-shadow: var(--tn-shadow-sm);
  overflow: hidden;
}
@media (min-width: 641px) {
  .fld-map {
    height: 60vh;
    height: min(60dvh, 560px);
    min-height: 380px;
  }
}
@media (min-width: 1024px) {
  /* The rail carries the navigation, so there is no bottom bar and more of the
     window is genuinely the map's to take. It still stops short of the fold. */
  .fld-map {
    height: 66vh;
    height: min(66dvh, 680px);
  }
}

/* The line under the card, where a map's attribution and its one instruction
   belong. Muted: it is read once, not once per record. */
.fld-map__caption {
  margin: 0;
  font-size: var(--tn-text-xs);
  color: var(--tn-color-text-muted);
}

/*
 * The chips along the top of the card: where this is, how much is on it, and
 * the one layer that can be switched. Small, opaque, and out of the way of the
 * imagery they label.
 */
.fld-map__chips {
  position: absolute;
  left: var(--tn-space-xs);
  right: var(--tn-space-xs);
  top: var(--tn-space-xs);
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-xxs);
  /* The strip is a layout box, not a target. Only the chips inside it take a
     tap, so the map can still be panned from between them. */
  pointer-events: none;
}
.fld-map__chip {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  /* One of these chips is a <button>, which does not inherit the page face. */
  font-family: inherit;
  line-height: var(--tn-leading-snug);
  /* The compact control height, not the 24px the text alone would give: one of
     these is a switch and a gloved thumb has to be able to hit it. */
  min-height: var(--tn-size-control-compact);
  padding: var(--tn-space-xxs) var(--tn-space-sm);
  background: var(--tn-color-surface-overlay);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-pill);
  box-shadow: var(--tn-shadow-sm);
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fld-map__chip--toggle {
  pointer-events: auto;
  color: var(--tn-color-accent);
  cursor: pointer;
}
.fld-map__chip--toggle[aria-pressed='false'] { color: var(--tn-color-text-muted); }
.fld-map__chip--toggle:disabled { opacity: 0.6; cursor: default; }
.fld-map__chip--toggle:focus-visible {
  outline: 2px solid var(--tn-color-focus-ring);
  outline-offset: 2px;
}

/*
 * The bottom-left corner of the card, as one stack.
 *
 * The legend and the place-an-element button were separately positioned and
 * both anchored bottom-left, which on a narrow phone put one on top of the
 * other. A single column cannot collide with itself, and it is the arrangement
 * the reference uses: key above, action under the thumb.
 */
.fld-map__corner {
  position: absolute;
  left: var(--tn-space-xs);
  bottom: var(--tn-space-xs);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--tn-space-xs);
  max-width: calc(100% - var(--tn-space-xxl));
}

.fld-map__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-xs);
}

/* The palette opens over the map, anchored to the same corner as the button
   that opened it, and scrolls inside itself rather than growing the page. Its
   height is measured against the CARD now, not the viewport, or it would be
   clipped by the card's own overflow. */
.fld-map__sheet {
  position: absolute;
  left: var(--tn-space-xs);
  right: var(--tn-space-xs);
  bottom: calc(var(--tn-size-touch) + var(--tn-space-sm));
  z-index: 5;
  max-height: calc(100% - var(--tn-size-touch) - var(--tn-space-xxl));
  overflow-y: auto;
  overscroll-behavior: contain;
}
@media (min-width: 641px) {
  .fld-map__sheet { right: auto; width: min(560px, calc(100% - var(--tn-space-xxl))); }
}

.fld-map__canvas { position: absolute; inset: 0; }

/* The map is the one place in the platform where text sits over a photograph.
   A plain label would be unreadable over a white roof and over a shadow in the
   same frame, so every floating control gets an opaque surface of its own. */
.fld-map__float {
  position: absolute;
  z-index: 2;
  background: var(--tn-color-surface-overlay);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-lg);
  box-shadow: var(--tn-shadow-md);
}

/* The key, bottom left, as a short column of coloured dots. It sits in the
   corner stack, so it is positioned by that flex column and not by itself --
   position:static undoes the absolute it inherits from .fld-map__float. */
.fld-map__legend {
  position: static;
  max-width: 100%;
  padding: var(--tn-space-xs) var(--tn-space-sm);
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-xxs);
  font-size: var(--tn-text-xs);
  line-height: var(--tn-leading-tight);
  color: var(--tn-color-text-muted);
}
.fld-legend__key { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.fld-legend__swatch {
  flex: none;
  width: 10px; height: 10px; border-radius: 50%;
  border: 1px solid var(--tn-color-border-strong);
}

/* A notice about the map, above the map, in the page's own flow. Floated at the
   foot of the canvas it covered the legend and the place-an-element button --
   the two controls a surveyor reaches for while reading it. */
.fld-map__notice {
  display: block;
  padding: var(--tn-space-sm) var(--tn-space-md);
  border: 1px solid var(--tn-color-warning);
  border-left-width: 4px;
  border-radius: var(--tn-radius-md);
  background: var(--tn-color-warning-soft);
  color: var(--tn-color-warning-soft-ink);
  font-size: var(--tn-text-sm);
}

/* The placement banner. Deliberately loud: while it is up, a tap on the map
   creates a record, and a surveyor who does not know that will create one by
   trying to pan. */
.fld-map__arming {
  left: var(--tn-space-xs);
  right: var(--tn-space-xs);
  top: var(--tn-space-xs);
  display: flex;
  align-items: center;
  gap: var(--tn-space-sm);
  padding: var(--tn-space-sm);
  background: var(--tn-color-accent-soft);
  border-color: var(--tn-color-accent);
  color: var(--tn-color-accent-soft-ink);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
}
.fld-map__arming-text { flex: 1 1 auto; min-width: 0; }

.fld-map__crosshair {
  position: absolute;
  left: 50%; top: 50%;
  width: 46px; height: 46px;
  margin: -23px 0 0 -23px;
  border-radius: 50%;
  border: 2px solid var(--tn-color-accent);
  box-shadow: 0 0 0 2px var(--tn-color-surface-overlay);
  pointer-events: none;
  z-index: 3;
}
.fld-map__crosshair::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 6px; height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--tn-color-accent);
}


.fld-sheet {
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-lg);
  background: var(--tn-color-surface-raised);
  overflow: hidden;
}
.fld-sheet__head {
  display: flex;
  align-items: center;
  gap: var(--tn-space-sm);
  padding: var(--tn-space-sm) var(--tn-space-md);
  background: var(--tn-color-surface-sunken);
  border-bottom: 1px solid var(--tn-color-border);
}
.fld-sheet__title { font-size: var(--tn-text-base); font-weight: var(--tn-weight-semibold); }
.fld-sheet__body { padding: var(--tn-space-md); }

/* A real <fieldset> so the two pack buttons are announced as one group with a
   name. The reset is the price of that: a fieldset carries a border, a margin
   and a min-width the flex layout does not want. */
.fld-palette__packs {
  display: flex;
  gap: var(--tn-space-xs);
  margin: 0 0 var(--tn-space-md);
  padding: 0;
  border: 0;
  min-width: 0;
}
.fld-palette__pack {
  flex: 1 1 0;
  min-height: var(--tn-size-touch);
  border: 1px solid var(--tn-color-border-control);
  border-radius: var(--tn-radius-md);
  background: var(--tn-color-surface-raised);
  font-weight: var(--tn-weight-semibold);
  cursor: pointer;
}
.fld-palette__pack[aria-pressed='true'] {
  background: var(--tn-color-accent);
  border-color: var(--tn-color-accent);
  color: var(--tn-color-accent-ink);
}

.fld-palette__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--tn-space-xs);
  max-height: 44vh;
  overflow-y: auto;
  padding-right: 2px;
}
/* The icon sits BESIDE the two lines of text, not above them: the tile is a
   44px touch target either way, and a row reads left-to-right at a glance
   where a stack makes every tile taller for no gain in recognition. */
.fld-palette__item {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--tn-space-xs);
  min-height: var(--tn-size-touch);
  padding: var(--tn-space-xs) var(--tn-space-sm);
  text-align: left;
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-md);
  background: var(--tn-color-surface-raised);
  color: var(--tn-color-text);
  cursor: pointer;
}
.fld-palette__item:hover { background: var(--tn-color-accent-soft); }
.fld-palette__item[aria-pressed='true'] {
  border-color: var(--tn-color-accent);
  background: var(--tn-color-accent-soft);
  color: var(--tn-color-accent-soft-ink);
}
/* The class's own hue, from @tn/map-elements. The tile is the same swatch the
   map draws the placed element with, so recognising one is recognising both. */
.fld-palette__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 1px solid;
  border-radius: var(--tn-radius-sm);
}
/* The heading and coverage figures, below the map they describe. The gap comes
   from the map wrapper's own bottom margin now that the map is a card in the
   page rather than a full-bleed band, so nothing is added here. */
.fld-summary {
  display: flex;
  flex-direction: column;
}

.fld-chip__icon { display: inline-flex; line-height: 1; flex: none; }

/* Below 560px the masthead cannot hold the app name, the avatar, the sync
   chip AND a text button; the label goes and the door icon carries it. The
   text stays in the accessibility tree -- this is the tn-vh treatment, not
   display:none, so the button is still announced as "Sign out". */
@media (max-width: 559px) {
  /* Same treatment as sign-out: clipped, not removed, so the button is still
     announced as "Send now". */
  .fld-sendnow__label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* At 360px -- still a common Android width -- the app name, the state chip
     and two icon buttons come to within a few pixels of the viewport, and any
     rounding pushed them onto a second row. The gaps tighten and the name is
     allowed to shrink before anything wraps. */
  .tn-masthead { gap: var(--tn-space-xxs); }
  .tn-masthead__identity { flex: 0 1 auto; min-width: 0; }
  .tn-masthead__app { font-size: var(--tn-text-md); }
  .tn-masthead__side { flex: none; gap: var(--tn-space-xxs); }

  /* The avatar carries nothing on a phone: the name and role beside it are
     already hidden at this width, so it is a circle with a letter in it taking
     42px out of a masthead that has none to spare. */
  .tn-masthead__avatar { display: none; }

  .fld-signout__label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}
.fld-palette__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.fld-palette__name { font-weight: var(--tn-weight-semibold); font-size: var(--tn-text-sm); }
.fld-palette__meta { font-size: var(--tn-text-xs); color: var(--tn-color-text-faint); }


.fld-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--tn-space-xs);
}
.fld-stat {
  padding: var(--tn-space-sm) var(--tn-space-md);
  background: var(--tn-color-surface-raised);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-lg);
}
.fld-stat__value {
  display: block;
  font-size: var(--tn-text-xxl);
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: var(--tn-leading-tight);
  color: var(--tn-color-text);
}
.fld-stat__label {
  display: block;
  margin-top: 2px;
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}

.fld-meter {
  height: 10px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-color-surface-sunken);
  border: 1px solid var(--tn-color-border);
  overflow: hidden;
}
.fld-meter__fill {
  height: 100%;
  background: var(--tn-color-status-approved);
  transition: width var(--tn-duration-slow) var(--tn-easing-standard);
}

.fld-pulse {
  position: relative;
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--tn-color-status-pending);
  flex: 0 0 auto;
}
.fld-pulse::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid var(--tn-color-status-pending);
  animation: fld-ring 1800ms var(--tn-easing-standard) infinite;
}
@keyframes fld-ring {
  0%   { transform: scale(0.6); opacity: 0.9; }
  100% { transform: scale(1.5); opacity: 0; }
}

.fld-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-xs);
  min-height: 32px;
  padding: 0 var(--tn-space-sm);
  border-radius: var(--tn-radius-pill);
  border: 1px solid var(--tn-color-border-strong);
  background: var(--tn-color-neutral-soft);
  color: var(--tn-color-neutral-soft-ink);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
}
.fld-chip--held { background: var(--tn-color-status-pending-soft); color: var(--tn-color-status-pending-ink); }
.fld-chip--sending { background: var(--tn-color-accent-soft); color: var(--tn-color-accent-soft-ink); }
.fld-chip--saved { background: var(--tn-color-status-approved-soft); color: var(--tn-color-status-approved-ink); }
/* DANGER, not status-rejected. "Sent back" is a supervisor's decision about a
   record and is violet on the map; "refused" is the service rejecting an
   upload, which is an error and must read as one. They were the same token
   until the map palette moved, at which point this chip quietly went lavender. */
.fld-chip--failed { background: var(--tn-color-danger-soft); color: var(--tn-color-danger-soft-ink); }

/*
 * The bar the sync chip sits in, at the top of every field screen.
 *
 * NEW ON THE MERGED WEBSITE, and it exists because the chip lost its old home.
 * In the surveyor app it was passed to AppShell as a masthead action; that slot
 * is shared with four other sections now and carries sign-out. So the chip has a
 * strip of its own, directly under the masthead and above the page heading,
 * where a surveyor's eye already goes to read "has my work gone in?".
 *
 * Sticky rather than fixed: it must stay visible while a surveyor scrolls a
 * 31-field form, and a fixed strip would sit over the map on a screen whose one
 * job is to show the map.
 *
 * THE TOP OFFSET IS THE CHROME, NOT ZERO, and the difference is the whole
 * behaviour. The page scrolls on the document, not in a scroll container the
 * shell owns, and the masthead above is itself sticky at top 0 on z-index 200.
 * So a syncbar stuck at top 0 on z-index 3 did not tuck under the masthead --
 * it went BEHIND it, and the one strip on the platform allowed to say whether a
 * record has reached the server vanished the moment a surveyor scrolled past the
 * first field of a 31-field form. --tn-chrome-top is the shell's own name for
 * the height of whatever is fixed above the content: the masthead on a phone,
 * the top bar on the desktop rail, plus the environment banner where there is
 * one. The map already sizes itself against it.
 *
 * NO BACKTICKS ANYWHERE IN THIS FILE'S COMMENTS: every block in it is a template
 * literal, and one backtick in prose ends the stylesheet mid-rule.
 */
.fld-syncbar {
  position: sticky;
  top: var(--tn-chrome-top);
  /* The platform's sticky layer, not a hand-picked 3. The map card is a
     position:relative box with no stacking context of its own, so its chips at
     z-index 3 tied with this bar and, coming later in the document, painted
     over it the moment the card scrolled up underneath. Sticky sits below the
     header (200) and below a toast (400), which is the order that is wanted. */
  z-index: var(--tn-z-sticky);
  display: flex;
  justify-content: flex-end;
  gap: var(--tn-space-xs);
  margin-bottom: var(--tn-space-sm);
  /* The strip has to be opaque for its whole height or the content scrolling
     beneath shows through the gap between it and the masthead. */
  padding: var(--tn-space-xs) 0;
  background: var(--tn-color-surface);
}
@media (max-width: 640px) {
  /* On a phone the chip goes to the left edge, where the thumb is, and the bar
     spans the full width so the strip does not read as a floating pill. */
  .fld-syncbar { justify-content: flex-start; }
}


/*
 * A CAPPED MEASURE. The form is the shell's full width, and on a 1900px panel
 * that put two columns of inputs a foot apart with a metre of nothing to their
 * right — a layout that is harder to fill in than the phone it was designed
 * for, because the eye has to travel back across the whole screen between the
 * label and the next question.
 *
 * 940px holds two comfortable columns and stops. It is not centred: the form
 * stays left-aligned with the heading above it, because a block that centres
 * itself under a left-aligned title reads as a different page.
 */
/*
 * THE MEASURE WIDENS WITH THE SCREEN, IN COLUMNS.
 *
 * 940px was right when a field was a label floating above a separate input:
 * the eye had to travel from the question to the answer and back, and across a
 * 1900px panel that trip was the whole problem. Boxing the label into the
 * control removed it -- each field is now one self-contained object -- so the
 * cap was buying nothing and costing ~700px of empty page beside every form.
 *
 * The grid below adds a THIRD column past 1400px rather than making two columns
 * wider, because a text input a foot wide is harder to read back than two that
 * are not, and more fields on screen is less scrolling in the field.
 */
.fld-form { padding-bottom: 96px; max-width: 1240px; }
@media (min-width: 1600px) { .fld-form { max-width: 1560px; } }

/*
 * EACH SECTION IS A CARD.
 *
 * The fields sat directly on the page ground with nothing but an uppercase word
 * and a hairline between one group and the next, so a 31-field form read as one
 * undifferentiated column of boxes floating on lavender -- no edge anywhere to
 * say where Identity stopped and Classification began, and nothing to rest the
 * eye on while filling it in.
 *
 * A card per section gives the group a real boundary, puts the questions on
 * white where the input borders actually register, and makes the section
 * heading a header rather than a caption that happens to sit above things.
 */
.fld-form__group {
  margin: 0 0 var(--tn-space-md);
  padding: 0;
  border: 1px solid var(--tn-color-border);
  border-radius: var(--tn-radius-lg);
  background: var(--tn-color-surface-raised);
  box-shadow: var(--tn-shadow-sm);
  /* A fieldset carries a min-width that stops its grid from shrinking. */
  min-width: 0;
}
/*
 * The header carries the section's NAME and its PROGRESS, and it is tinted with
 * the accent rather than filled grey. Grey-on-white read as disabled: eight
 * identical dead bars down a form that a surveyor is meant to work through.
 * The rule beneath is the accent at low opacity, which separates the header
 * from the fields without a second hard border.
 */
.fld-form__legend {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-sm);
  width: 100%;
  margin: 0;
  padding: var(--tn-space-sm) var(--tn-space-lg);
  border-bottom: 1px solid color-mix(in srgb, var(--tn-color-accent) 22%, transparent);
  background: var(--tn-color-accent-soft);
  border-radius: var(--tn-radius-lg) var(--tn-radius-lg) 0 0;
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--tn-color-accent-soft-ink);
}
/* The fields live inside the card, so the padding is here rather than on the
   card itself -- the legend needs to reach the card's edges to read as a
   header, and it cannot if its parent is padded. */
.fld-form__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 var(--tn-space-lg);
  padding: var(--tn-space-lg) var(--tn-space-lg) var(--tn-space-xs);
}
@media (min-width: 900px) {
  .fld-form__grid { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 1400px) {
  .fld-form__grid { grid-template-columns: repeat(3, 1fr); }
}
/* The last row's field carries a bottom margin that would otherwise show as a
   gap between the final control and the card's edge. */
.fld-form__grid > .tn-field:last-child { margin-bottom: var(--tn-space-sm); }

/*
 * ANYTHING IN A SECTION CARD THAT IS NOT THE FIELD GRID.
 *
 * The card's padding lives on .fld-form__grid rather than on the card itself,
 * because the header has to reach the card's edges to read as a header. That is
 * right for the eight generated sections and it silently broke every section
 * that holds something else: the Photographs card put the kind selector, the
 * camera buttons, the thumbnails and the count HARD AGAINST the card's border,
 * with the capture fieldset's own rounded corners cutting across the card's --
 * the one part of this screen that looked unfinished rather than merely plain.
 * The note under an element's photograph group had the same fault.
 *
 * So the padding is a class, applied to whatever is not the grid.
 */
.fld-form__body {
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-md);
  padding: var(--tn-space-lg);
}
/* After the grid, the grid's own bottom padding already stands in for the top,
   and doubling it opens a band across the middle of the card. */
.fld-form__grid + .fld-form__body { padding-top: var(--tn-space-sm); }

/* The count. Tabular figures so 2/5 and 11/15 do not shuffle the header, and a
   green fill the moment a section is finished -- the one piece of feedback that
   makes working down a 31-field form feel like progress rather than a list. */
.fld-form__count {
  flex: none;
  padding: 2px var(--tn-space-xs);
  border-radius: var(--tn-radius-pill);
  background: var(--tn-color-surface-raised);
  border: 1px solid color-mix(in srgb, var(--tn-color-accent) 30%, transparent);
  color: var(--tn-color-accent-soft-ink);
  font-size: var(--tn-text-xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
.fld-form__count.is-complete {
  background: var(--tn-color-status-approved-soft);
  border-color: color-mix(in srgb, var(--tn-color-status-approved) 40%, transparent);
  color: var(--tn-color-status-approved-ink);
}
.fld-form__legend-name { min-width: 0; }

/* Inputs on a faintly sunken ground rather than white-on-white. Inside a white
   card an outlined white box has only its 1px border to say it is a control;
   the tint gives it an edge that survives a glare-washed panel, and it turns
   white on focus so the active field is the brightest thing in the section. */
/* The BOX carries the surface now, not the input inside it. */
.fld-form__grid .tn-field__box { background: var(--tn-color-surface); }
.fld-form__grid .tn-field__box:focus-within { background: var(--tn-color-surface-raised); }

/*
 * A REQUIRED FIELD IS RED HERE, AND ONLY HERE.
 *
 * @tn/ui marks required with an asterisk and nothing else, and the note in its
 * stylesheet says why: sign-in is two fields, both required, so colouring the
 * box turned a page in a perfectly ordinary state into two validation errors
 * before anyone had typed. That reasoning holds for a two-field page and stops
 * holding on a card of thirty-one, where an asterisk 2mm wide is the only thing
 * separating the questions that must be answered from the ones that need not
 * be, read at arm's length in direct sun on a dam crest.
 *
 * So the treatment is scoped to the capture grid rather than pushed down into
 * the component: a red label and a red edge on the box, which is what a red
 * asterisk was standing in for. Everything else on the platform keeps the
 * asterisk. :has reaches the box from the marker the component already renders
 * inside the label, so nothing has to be threaded through a second prop.
 *
 * IT IS DELIBERATELY INERT TODAY. Not one of the client's 31 building fields or
 * 719 registry fields is declared mandatory -- their workbook has no such
 * column and the survey service enforces none -- and inventing a required
 * field here would be inventing a government rule. The rule below is what
 * renders the day they tell us which fields those are.
 */
.fld-form__grid .tn-field__box:has(.tn-field__required) {
  border-color: var(--tn-color-danger);
  background: var(--tn-color-danger-soft);
}
.fld-form__grid .tn-field__box:has(.tn-field__required):focus-within {
  border-color: var(--tn-color-danger);
  background: var(--tn-color-surface-raised);
  box-shadow: 0 0 0 3px var(--tn-color-danger-soft);
}
.fld-form__grid .tn-field__label:has(.tn-field__required) {
  color: var(--tn-color-danger-soft-ink);
}

/* A read-only value is NOT an empty input, and it must not be a second box
   inside the field's box either. The VALUE is drawn plainly; the state is said
   by tinting the whole box below, so a scan down the column can tell at a
   glance which rows are asking for something and which are just reporting. */
.fld-readonly {
  display: block;
  min-height: 26px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--tn-color-text-muted);
  font-variant-numeric: tabular-nums;
}
/* :has is what lets the BOX carry the state without the component having to
   pass a modifier down for something only the surveyor's own control knows. */
.tn-field__box:has(.fld-readonly) {
  background: var(--tn-color-surface-sunken);
  border-style: dashed;
}

/* ------------------------------------------------------------- the queue */

/* The campus at a glance, above what is owed on it. */
.fld-queue__summary {
  padding: var(--tn-space-lg);
  border: 1px solid var(--tn-color-border);
  border-radius: var(--tn-radius-lg);
  background: var(--tn-color-surface-raised);
  box-shadow: var(--tn-shadow-sm);
}
.fld-queue__figures {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--tn-space-md);
  margin-bottom: var(--tn-space-md);
}
@media (min-width: 641px) {
  .fld-queue__figures { grid-template-columns: repeat(4, 1fr); }
}
.fld-queue__figure { display: flex; flex-direction: column; }
.fld-queue__n {
  font-size: var(--tn-text-xxl);
  font-weight: var(--tn-weight-semibold);
  line-height: var(--tn-leading-tight);
  font-variant-numeric: tabular-nums;
}
.fld-queue__l { font-size: var(--tn-text-sm); color: var(--tn-color-text-muted); }
.fld-queue__caption {
  margin-top: var(--tn-space-xxs);
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}

/* A section with nothing in it: one row, not a six-row empty state. */
.fld-queue__clear {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--tn-space-xs);
  padding: var(--tn-space-sm) var(--tn-space-md);
  border: 1px solid var(--tn-color-border);
  border-radius: var(--tn-radius-lg);
  background: var(--tn-color-surface-raised);
}
.fld-queue__tick {
  flex: none;
  color: var(--tn-color-status-approved-ink);
  font-weight: var(--tn-weight-semibold);
}
.fld-queue__clear-title { font-weight: var(--tn-weight-semibold); font-size: var(--tn-text-sm); }
.fld-queue__clear-note { font-size: var(--tn-text-sm); color: var(--tn-color-text-muted); }

.fld-queue__body { padding: var(--tn-space-md); }

/* Send everything now, and the offline line beside it. On a phone the button
   goes full width: it is the one action on the queue that cannot wait, and a
   button the width of its own label in the corner of a section does not read
   as one. Above 640px it takes its natural width with the line beside it. */
.fld-queue__send {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tn-space-xs);
  margin-bottom: var(--tn-space-sm);
}
@media (max-width: 640px) {
  .fld-queue__send .tn-btn { width: 100%; }
}

.fld-savebar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--tn-z-sticky);
  display: flex;
  align-items: center;
  gap: var(--tn-space-sm);
  /* A KNOWN HEIGHT, because two other fixed things on this screen have to be
     placed relative to it: the toast viewport below, and the form's own bottom
     padding. 52px of large button plus 12px of padding either side. */
  min-height: 76px;
  padding: var(--tn-space-sm) var(--tn-space-md);
  padding-bottom: calc(var(--tn-space-sm) + env(safe-area-inset-bottom, 0px));
  background: var(--tn-color-surface-overlay);
  border-top: 1px solid var(--tn-color-border-strong);
  box-shadow: var(--tn-shadow-lg);
}
@media (max-width: 640px) {
  .fld-savebar {
    bottom: calc(var(--tn-size-bottom-nav-height) + env(safe-area-inset-bottom, 0px));
    padding-bottom: var(--tn-space-sm);
  }
}

/*
 * THE TOAST MUST CLEAR THE SAVE BAR.
 *
 * @tn/ui puts the toast viewport at the bottom right of the window on z-index
 * 400, and on a phone just above the bottom navigation. The save bar is fixed
 * in exactly that band on z-index 100, so on the one screen where both appear
 * the toast covered the bar completely -- including the Save button. That is
 * not a flicker: a held or refused save is shown with durationMs null, so the
 * toast stays until it is dismissed and the surveyor is looking at a Save
 * button they cannot see or press.
 *
 * 88px is the bar (76px, capped above) plus a gap. The selector is scoped by
 * :has so it applies on the capture screen and nowhere else -- the map and the
 * queue have no save bar and their toasts should sit where the platform puts
 * every other toast.
 */
body:has(.fld-savebar) .tn-toast-viewport {
  bottom: calc(88px + env(safe-area-inset-bottom, 0px));
}
@media (max-width: 640px) {
  body:has(.fld-savebar) .tn-toast-viewport {
    bottom: calc(var(--tn-size-bottom-nav-height) + 88px + env(safe-area-inset-bottom, 0px));
  }
}

/* Clear of the rail. The bar is fixed to the VIEWPORT, so with left:0 it ran
   under the sidebar -- "No changes yet" sat on the dark ground outside the
   column it belongs to, where it could barely be read. There is no bottom
   navigation at this width either, so it returns to the foot of the window. */
@media (min-width: 1024px) {
  .fld-savebar {
    left: 264px;
    bottom: 0;
    padding-left: var(--tn-space-lg);
    padding-right: var(--tn-space-lg);
  }
}
/* The bar spans the window, but its CONTENTS track the form above it. Stretched
   across 1900px the state sat hard left and Save hard right, a foot apart, with
   nothing to say they belonged to the same record. */
.fld-savebar__inner {
  display: flex;
  align-items: center;
  gap: var(--tn-space-sm);
  width: 100%;
  max-width: 1240px;
}
.fld-savebar__state { flex: 1 1 auto; min-width: 0; font-size: var(--tn-text-sm); }
.fld-savebar__state strong { display: block; font-size: var(--tn-text-base); }
/* The second line is a refusal message from the service and can be a
   paragraph. Two lines, then an ellipsis: the bar has a declared height that
   the toast and the form's bottom padding are both measured against, and a
   four-line message would grow it past both and hide the Save button it sits
   beside. The full message is on the queue, with the record it belongs to. */
.fld-savebar__state span {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.fld-photos { display: flex; flex-wrap: wrap; gap: var(--tn-space-xs); }
.fld-photo {
  width: 92px;
  height: 92px;
  border-radius: var(--tn-radius-md);
  border: 1px solid var(--tn-color-border-strong);
  background: var(--tn-color-surface-sunken);
  object-fit: cover;
}


.fld-list { list-style: none; display: flex; flex-direction: column; gap: var(--tn-space-xs); }
/*
 * A ROW WRAPS BEFORE IT CRUSHES ITS OWN TEXT.
 *
 * A held record carries two buttons -- Try again and Discard -- and on a 390px
 * phone the two of them plus the row's padding leave about 110px for the
 * record's name and its state. The row did not wrap, so "ALY-094 - Record -
 * recorded 14 min ago - 3 attempts" was squeezed into a four-line ribbon a
 * finger wide beside two buttons, and the refusal message underneath it with
 * it. Wrapping puts the buttons on their own line when they do not fit, which
 * is also the arrangement the reference uses for a stacked action.
 */
.fld-list__item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--tn-space-sm);
  min-height: var(--tn-size-touch);
  padding: var(--tn-space-sm) var(--tn-space-md);
  background: var(--tn-color-surface-raised);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-lg);
}
/* 12rem is the width below which the label stops being readable rather than
   merely narrow; under it the row breaks instead of squeezing. */
.fld-list__main { flex: 1 1 12rem; min-width: 0; }

/*
 * 44px, EVEN AT size="sm".
 *
 * @tn/ui's compact button is 36px, which is right on a desk and eight pixels
 * short of the platform's own touch floor. Every action a surveyor takes on the
 * queue -- Open, Start, Try again, Discard -- is one of these, taken one-handed
 * with a phone in a glove or in rain. The floor applies on the field screens;
 * the administration tables keep the compact height they were designed for.
 */
.fld-list__item .tn-btn--sm { min-height: var(--tn-size-touch); }
.fld-list__title { font-weight: var(--tn-weight-semibold); }
.fld-list__sub { font-size: var(--tn-text-sm); color: var(--tn-color-text-muted); }
.fld-list__link { text-decoration: none; color: inherit; }
.fld-list__link:hover .fld-list__title { text-decoration: underline; }

/* The row's actions as one group, so a wrap moves both together and never
   strands one button on a line of its own. */
.fld-list__actions {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tn-space-xs);
}

/**
 * The panel that opens when a footprint or a placed element is tapped.
 *
 * A CARD, NOT A ROW. What a surveyor needs at the moment of tapping is the
 * asset's identity and one obvious next action, and the previous single-line
 * row could carry neither: the area and the gis id were folded into one muted
 * sentence, and "Open record" said the same thing whether a record existed or
 * not. Labelled rows survive being read one-handed in sun; the action names
 * what it will actually do.
 *
 * IT FLOATS OVER THE CANVAS, because a panel below the fold is a panel nobody
 * sees -- a card underneath the map opened off-screen and the tap appeared to
 * do nothing at all.
 *
 * ON A PHONE IT IS FIXED TO THE WINDOW, NOT TO THE MAP CARD, AND THAT IS THE
 * BUG SUDHARSAN REPORTED. Anchored inside the card it was clipped by the card's
 * own overflow: the sentence explaining the state was cut off mid-word and the
 * start-survey button -- the primary action of the entire application -- sat
 * behind the bottom tab bar where it could not be pressed. Fixed to the window
 * and lifted by the tab bar's height plus the home-indicator inset, exactly as
 * the capture form's save bar is, it clears the navigation at any scroll
 * position and can no longer be cropped by an ancestor.
 *
 * ON A WIDE SCREEN it goes back inside the card, top right, at a fixed column
 * width. There is no bottom bar at that size, there is room beside the map
 * rather than on top of it, and a panel stretched across a 1900px window would
 * put its label and its value a foot apart.
 *
 * The head and the actions are pinned; only the middle scrolls. However long a
 * feature class name is, the button is on the screen.
 *
 * IT REMAINS A CHILD OF THE MAP CARD so the wide-screen rule has something to
 * be absolute against, and the card's overflow:hidden does not clip it while it
 * is fixed -- overflow only clips a fixed descendant when the ancestor is its
 * containing block, which needs a transform, a filter, perspective, contain or
 * will-change, and .fld-map has none of them. Add one to that rule and this
 * panel is clipped again, which is the exact fault being fixed here.
 */
.fld-info {
  position: fixed;
  left: var(--tn-space-sm);
  right: var(--tn-space-sm);
  bottom: calc(var(--tn-size-bottom-nav-height) + env(safe-area-inset-bottom, 0px) + var(--tn-space-xs));
  z-index: var(--tn-z-sticky);
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-sm);
  padding: var(--tn-space-md);
  background: var(--tn-color-surface-overlay);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-xl);
  box-shadow: var(--tn-shadow-lg);
  max-height: calc(100dvh - var(--tn-chrome-top) - var(--tn-size-bottom-nav-height) - var(--tn-space-xl));
  overscroll-behavior: contain;
}
@media (min-width: 641px) {
  .fld-info {
    position: absolute;
    left: auto;
    bottom: auto;
    top: var(--tn-space-xs);
    right: var(--tn-space-xs);
    width: 360px;
    max-width: calc(100% - var(--tn-space-md));
    max-height: calc(100% - var(--tn-space-md));
    padding: var(--tn-space-lg);
    gap: var(--tn-space-md);
  }
}

/* Only the middle scrolls. min-height:0 is what lets a flex child shrink below
   its content and actually become the scroller. */
.fld-info__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/*
 * The actions, stacked and full width, ranked by what they do.
 *
 * Stacked rather than in a row: each one is then a full-width target for a
 * thumb in sunlight, and the order down the card is the order of importance --
 * the filled button starts or opens the record, the outlined one only moves the
 * map. Side by side they would read as equals.
 */
.fld-info__actions {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-xs);
  padding-top: var(--tn-space-xs);
  border-top: 1px solid var(--tn-color-border);
}
.fld-info__head {
  display: flex;
  align-items: center;
  gap: var(--tn-space-sm);
}
.fld-info__title {
  flex: 1 1 auto;
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--tn-space-xs);
  font-size: var(--tn-text-lg);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-accent);
}
.fld-info__close {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  font-size: 22px;
  line-height: 1;
  color: var(--tn-color-text-muted);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-md);
  cursor: pointer;
}
.fld-info__close:hover {
  color: var(--tn-color-text);
  background: var(--tn-color-surface-sunken);
}
.fld-info__close:focus-visible {
  outline: 2px solid var(--tn-color-focus-ring);
  outline-offset: 2px;
}

.fld-info__rows { margin: 0; display: flex; flex-direction: column; }
/* A hairline between rows, never above the first or below the last. */
.fld-info__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-space-md);
  padding: var(--tn-space-sm) 0;
  border-top: 1px solid var(--tn-color-border);
}
.fld-info__row:first-child { border-top: 0; }
.fld-info__label {
  color: var(--tn-color-text-muted);
  font-size: var(--tn-text-sm);
}
/* The value is the thing being read, so it carries the weight, and it aligns
   right so a column of them scans as a column. Digits are tabular for the
   same reason -- an area of 4.1 and one of 672.3 line up on the decimal. */
.fld-info__value {
  margin: 0;
  text-align: right;
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.fld-info__note {
  margin: 0;
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}

/**
 * The photograph capture control.
 *
 * The native file input is hidden rather than removed -- it is still the thing
 * that opens the camera, and wrapping it in its own <label> means the whole
 * button is the activator with no JavaScript and no loss of keyboard or
 * screen-reader behaviour. It is clipped to 1px rather than display:none,
 * because a display:none input is skipped by some browsers' focus order and by
 * at least one Android WebView's file picker.
 *
 * THE TWO BUTTONS STACK ON A PHONE AND SIT SIDE BY SIDE ON A DESKTOP. Stacked,
 * each is a full-width target for a thumb in sun; side by side, two full-width
 * buttons across a 1400px form would look absurd and read as more important
 * than the 31 fields above them.
 */
.fld-capture {
  margin: 0;
  padding: var(--tn-space-md);
  border: 1px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-lg);
  background: var(--tn-color-surface-raised);
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-sm);
}
.fld-capture__legend {
  padding: 0 var(--tn-space-xs);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-text-muted);
}

.fld-capture__actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--tn-space-sm);
}
@media (min-width: 641px) {
  .fld-capture__actions { grid-template-columns: 1fr 1fr; }
}

.fld-capture__btn {
  /* The hidden file input inside is position:absolute. Without a positioned
     ancestor it resolved against the nearest one further up -- or the initial
     containing block -- so focusing the camera button by keyboard scrolled the
     page to a 1px box somewhere else entirely. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--tn-space-xs);
  min-height: var(--tn-size-touch);
  padding: var(--tn-space-sm) var(--tn-space-md);
  font-size: var(--tn-text-md);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-accent);
  background: var(--tn-color-surface-raised);
  border: 1px solid var(--tn-color-accent);
  border-radius: var(--tn-radius-md);
  cursor: pointer;
  text-align: center;
}
.fld-capture__btn:hover { background: var(--tn-color-accent-soft); }
/* The camera is the one a surveyor standing at the asset wants, so it is the
   filled button and the gallery is the outline. */
.fld-capture__btn--camera {
  color: var(--tn-color-accent-ink);
  background: var(--tn-color-accent);
}
.fld-capture__btn--camera:hover { background: var(--tn-color-accent-hover); }
.fld-capture__btn svg { width: 20px; height: 20px; flex: none; }

/* Focus lands on the input, so the ring has to be drawn on the label. */
.fld-capture__btn:focus-within {
  outline: 2px solid var(--tn-color-focus-ring);
  outline-offset: 2px;
}
.fld-capture__btn input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.fld-capture__hint,
.fld-capture__fix {
  margin: 0;
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}
.fld-capture__fix { font-variant-numeric: tabular-nums; color: var(--tn-color-text); }
.fld-capture__fix.is-none { color: var(--tn-color-text-muted); }
.fld-capture__accuracy { color: var(--tn-color-text-muted); }

.fld-note {
  display: block;
  margin-bottom: var(--tn-space-md);
  padding: var(--tn-space-sm) var(--tn-space-md);
  border-left: 4px solid var(--tn-color-accent);
  background: var(--tn-color-accent-soft);
  color: var(--tn-color-accent-soft-ink);
  border-radius: var(--tn-radius-md);
  font-size: var(--tn-text-sm);
}
.fld-note--warn {
  border-left-color: var(--tn-color-warning);
  background: var(--tn-color-warning-soft);
  color: var(--tn-color-warning-soft-ink);
}


@media (prefers-reduced-motion: reduce) {
  .fld-pulse::after { animation: none; opacity: 0.5; transform: none; }
  .fld-meter__fill { transition: none; }
}

@media (forced-colors: active) {
  .fld-map__float, .fld-map__chip, .fld-info, .fld-stat, .fld-list__item, .fld-sheet {
    border: 1px solid CanvasText;
  }
  .fld-pulse::after { animation: none; }
}

@media print {
  .fld-map, .fld-map__caption, .fld-savebar, .fld-sheet__head { display: none !important; }
  .fld-form { padding-bottom: 0; }
}

/*
 * THE PALETTE OWNS THE CARD WHILE IT IS OPEN.
 *
 * Everything drawn over the map is absolutely positioned inside the same card:
 * the context chips top-left, the legend and the action button bottom-left in
 * .fld-map__corner, the locate/zoom controls bottom-right, and the palette
 * sheet itself. On a desktop there is room for all of them. On a 390px phone
 * there is not, and measured on the deployed pilot with the palette open there
 * were SIX overlapping pairs — the chips over the sheet, the sheet over the
 * legend, the legend over the class list, the whole corner over everything.
 * A surveyor trying to read "Catchment boundary" got the status legend printed
 * across it.
 *
 * A phone has one thing on screen at a time. So while the palette is open the
 * other overlays leave: they are not information the surveyor needs while
 * choosing a class, and they come straight back on close. The palette keeps its
 * own close button, so nothing is stranded.
 *
 * Above 640px the card is wide enough for the sheet to sit beside them and they
 * all stay, which is why this is inside the media query.
 */
@media (max-width: 640px) {
  /* NOT .fld-map__float — that is a SHARED modifier the sheet carries too
     (fld-map__float fld-map__sheet), so hiding it hides the palette itself.
     Measured: doing that left zero overlaps and zero palette. The legend lives
     inside .fld-map__corner, so hiding the corner takes it with it. */
  .fld-map[data-palette='open'] .fld-map__chips,
  .fld-map[data-palette='open'] .fld-map__corner {
    display: none;
  }
  /* With the others gone the sheet can have the whole card, which is what makes
     a 12-class list readable instead of a 95px letterbox. */
  .fld-map[data-palette='open'] .fld-map__sheet {
    top: var(--tn-space-xs);
    bottom: var(--tn-space-xs);
    left: var(--tn-space-xs);
    right: var(--tn-space-xs);
    max-height: none;
  }
}
