:root {
  color-scheme: light;
  --tn-color-surface: #f4f6fa;
  --tn-color-surface-raised: #ffffff;
  --tn-color-surface-sunken: #e8ecf4;
  --tn-color-surface-overlay: #ffffff;
  --tn-color-header-bg: #14224a;
  --tn-color-header-ink: #ffffff;
  --tn-color-header-ink-muted: #bcc8e4;
  --tn-color-header-border: #26355f;
  --tn-color-text: #0f1a33;
  --tn-color-text-muted: #434e68;
  --tn-color-text-faint: #566079;
  --tn-color-text-inverse: #ffffff;
  --tn-color-border: #d8dee9;
  --tn-color-border-strong: #b4bed0;
  --tn-color-border-control: #6b7690;
  --tn-color-accent: #1e40af;
  --tn-color-accent-hover: #1a3690;
  --tn-color-accent-active: #14224a;
  --tn-color-accent-ink: #ffffff;
  --tn-color-accent-soft: #e6ecf9;
  --tn-color-accent-soft-ink: #1b3a8f;
  --tn-color-action: #0f6d64;
  --tn-color-action-hover: #0b564f;
  --tn-color-action-active: #083f3a;
  --tn-color-action-ink: #ffffff;
  --tn-color-action-soft: #dbefea;
  --tn-color-action-soft-ink: #0a544d;
  --tn-color-danger: #9c2f26;
  --tn-color-danger-hover: #82251d;
  --tn-color-danger-ink: #ffffff;
  --tn-color-danger-soft: #fbe8e5;
  --tn-color-danger-soft-ink: #8a271e;
  --tn-color-warning: #8a5a0c;
  --tn-color-warning-ink: #ffffff;
  --tn-color-warning-soft: #fbf1de;
  --tn-color-warning-soft-ink: #7a4a08;
  --tn-color-success: #1f6b43;
  --tn-color-success-ink: #ffffff;
  --tn-color-success-soft: #e2f1e8;
  --tn-color-success-soft-ink: #1d5636;
  --tn-color-neutral-soft: #e8ecf3;
  --tn-color-neutral-soft-ink: #465068;
  --tn-color-status-unsurveyed: #c8362b;
  --tn-color-status-unsurveyed-soft: #fce9e6;
  --tn-color-status-unsurveyed-ink: #8f2419;
  --tn-color-status-pending: #b87a0d;
  --tn-color-status-pending-soft: #fdf1d8;
  --tn-color-status-pending-ink: #7a4a08;
  --tn-color-status-approved: #17845a;
  --tn-color-status-approved-soft: #e0f1e9;
  --tn-color-status-approved-ink: #1d5636;
  --tn-color-status-rejected: #7c3fae;
  --tn-color-status-rejected-soft: #f1e8fa;
  --tn-color-status-rejected-ink: #59277f;
  --tn-color-focus-ring: #1e40af;
  --tn-color-focus-ring-offset: #ffffff;
  --tn-color-scrim: rgba(10, 18, 40, 0.52);
  --tn-color-skeleton-base: #e1e6ef;
  --tn-color-skeleton-sheen: #f4f6fa;
  --tn-space-none: 0;
  --tn-space-xxs: 4px;
  --tn-space-xs: 8px;
  --tn-space-sm: 12px;
  --tn-space-md: 16px;
  --tn-space-lg: 24px;
  --tn-space-xl: 32px;
  --tn-space-xxl: 40px;
  --tn-space-xxxl: 52px;
  --tn-space-section: 64px;
  --tn-space-page: 88px;
  --tn-text-xs: 12px;
  --tn-text-sm: 13px;
  --tn-text-base: 14px;
  --tn-text-md: 16px;
  --tn-text-lg: 18px;
  --tn-text-xl: 20px;
  --tn-text-xxl: 24px;
  --tn-text-xxxl: 30px;
  --tn-leading-tight: 1.25;
  --tn-leading-snug: 1.35;
  --tn-leading-normal: 1.5;
  --tn-leading-relaxed: 1.65;
  --tn-weight-regular: 400;
  --tn-weight-medium: 500;
  --tn-weight-semibold: 600;
  --tn-weight-bold: 700;
  --tn-tracking-wide: 0.04em;
  --tn-tracking-wider: 0.08em;
  --tn-tracking-normal: 0;
  --tn-tracking-tight: -0.01em;
  --tn-tracking-tighter: -0.02em;
  --tn-font-sans: 'Segoe UI Variable Text', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans', 'Helvetica Neue', Arial, 'Noto Sans Tamil', 'Nirmala UI', Latha, sans-serif;
  --tn-font-mono: ui-monospace, 'Cascadia Mono', 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Roboto Mono', 'Liberation Mono', 'Noto Sans Mono', monospace;
  --tn-radius-none: 0;
  --tn-radius-sm: 5px;
  --tn-radius-md: 10px;
  --tn-radius-lg: 16px;
  --tn-radius-xl: 24px;
  --tn-radius-pill: 999px;
  --tn-shadow-none: none;
  --tn-shadow-xs: 0 1px 2px rgba(15, 26, 51, 0.06);
  --tn-shadow-sm: 0 1px 2px rgba(15, 26, 51, 0.06), 0 2px 6px rgba(15, 26, 51, 0.05);
  --tn-shadow-md: 0 1px 2px rgba(15, 26, 51, 0.06), 0 6px 16px -4px rgba(15, 26, 51, 0.10);
  --tn-shadow-lg: 0 2px 4px rgba(15, 26, 51, 0.06), 0 16px 32px -8px rgba(15, 26, 51, 0.16);
  --tn-shadow-xl: 0 4px 8px rgba(15, 26, 51, 0.08), 0 24px 48px -12px rgba(15, 26, 51, 0.24);
  --tn-shadow-focus: 0 0 0 3px rgba(30, 64, 175, 0.22);
  --tn-size-touch: 44px;
  --tn-size-control: 40px;
  --tn-size-control-compact: 32px;
  --tn-size-control-lg: 48px;
  --tn-size-header-height: 64px;
  --tn-size-bottom-nav-height: 64px;
  --tn-size-content-max: 1320px;
  --tn-size-prose-max: 68ch;
  --tn-size-stat-min: 152px;
  --tn-duration-fast: 120ms;
  --tn-duration-base: 180ms;
  --tn-duration-slow: 280ms;
  --tn-easing-standard: cubic-bezier(0.2, 0, 0, 1);
  --tn-z-base: 0;
  --tn-z-sticky: 100;
  --tn-z-header: 200;
  --tn-z-bottom-nav: 200;
  --tn-z-overlay: 300;
  --tn-z-toast: 400;
  --tn-z-skip-link: 500;
}

@media (max-width: 639.98px) {
  :root {
  --tn-text-sm: 14px;
  --tn-text-base: 16px;
  --tn-text-md: 17px;
  --tn-text-xxl: 22px;
  --tn-text-xxxl: 26px;
  }
}

@media (max-width: 639.98px), (pointer: coarse) {
  :root {
  --tn-size-control: 44px;
  --tn-size-control-compact: 44px;
  --tn-size-control-lg: 52px;
  }
}


*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* Anchored scrolling must not hide the target under the sticky header. */
  scroll-padding-top: calc(var(--tn-size-header-height) + var(--tn-space-md));
}

body {
  margin: 0;
  background: var(--tn-color-surface);
  color: var(--tn-color-text);
  font-family: var(--tn-font-sans);
  font-size: var(--tn-text-base);
  line-height: var(--tn-leading-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Kerning and contextual alternates on; the discretionary ligature that turns
     a plain "ffi" into a glyph a surveyor has to squint at, off. */
  font-kerning: normal;
  font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1;
  /* Segoe UI Variable and SF both carry an optical-size axis: asked, the
     browser opens up the 12-13px labels and tightens the display sizes, which
     is most of the difference between type that looks set and type that looks
     scaled. */
  font-optical-sizing: auto;
  font-synthesis-weight: none;
}

/*
 * Headings tighten as they get bigger. A 40px title tracked like 14px body copy
 * is the single most recognisable "nobody set this type" tell, and the platform
 * had letter-spacing on eyebrows and on nothing else.
 */
h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-weight: var(--tn-weight-semibold);
  line-height: var(--tn-leading-tight);
  /* Stops a two-word heading breaking one word onto its own line. */
  text-wrap: balance;
}
/* Optical tightening only where the type is big enough to need it: the page
   title and section titles. An 18px h3 tracked like a 30px figure looks
   pinched. */
h1, h2 { letter-spacing: var(--tn-tracking-tight); }
/* Prose gets the same courtesy one level down: no single-word last line. */
p { text-wrap: pretty; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; }

a { color: var(--tn-color-accent); text-underline-offset: 2px; }
/* :where() keeps this at element specificity. As a:hover (0,1,1) it beat every
   one-class link colour on hover, and turned the white quarters nav labels dark
   blue on the navy bar: invisible. A link that sets its own colour keeps it. */
a:where(:hover) { color: var(--tn-color-accent-hover); }

button, input, select, textarea { font: inherit; color: inherit; }
/* One input size on the platform: 14px on a desk, 16px on a phone, so iOS never
   zooms the viewport on focus. A module that sets a control's size must not go
   under 16px below 640px. */
input, select, textarea { font-size: var(--tn-text-base); }

/* The keyword bolder inside a semibold cell resolves to 900, which the census found on
   145 runs of /review. Strong is the bold step, never heavier. */
strong, b { font-weight: var(--tn-weight-bold); }

/* Figures that are compared down a column or against each other line up. */
table, .tn-num, [data-numeric], time, output { font-variant-numeric: tabular-nums lining-nums; }

:focus-visible {
  outline: 3px solid var(--tn-color-focus-ring);
  outline-offset: 2px;
  border-radius: var(--tn-radius-sm);
}
/* A ring on a rounded control must follow the corner it is drawn around, or it
   reads as a square box behind a pill. */
.tn-btn:focus-visible, .tn-status:focus-visible, .tn-badge:focus-visible { border-radius: inherit; }
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--tn-color-accent-soft); color: var(--tn-color-accent-soft-ink); }

/* Digits in identifiers, coordinates and measurements must line up column to
   column; proportional figures make two survey IDs of the same length look
   like different lengths. */
.tn-num, .tn-mono {
  font-variant-numeric: tabular-nums lining-nums;
}
.tn-mono { font-family: var(--tn-font-mono); }

/*
 * THE PROGRESS METER, AND WHY ITS WIDTH IS A CUSTOM PROPERTY WITH A FALLBACK.
 *
 * Every progress bar on this platform was a div with style={{ width: '37%' }}.
 * In production that attribute was refused by Content-Security-Policy (a nonce
 * authorises a <style> ELEMENT, never a style ATTRIBUTE), the declaration was
 * dropped, and the fill kept the 100% width it gets from its own rule. The
 * survey screen therefore printed a SOLID FULL-WIDTH BAR directly above the
 * sentence "1 of 270 footprints have a record — 0%". A government platform
 * reported a finished survey.
 *
 * The policy is fixed in middleware.ts, but the shape below is what makes the
 * fault unrepeatable. The fill takes its width from var(--tn-meter-fill, 0%).
 * A caller sets that one custom property; if the declaration is ever dropped
 * again — a stricter policy, a sanitiser, a browser bug — the fallback engages
 * and the bar reads EMPTY. The failure mode is now "reports nothing" instead of
 * "reports complete", which is the only acceptable direction for a number a
 * department will act on.
 */
.tn-meter {
  position: relative;
  height: 10px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-color-surface-sunken);
  box-shadow: inset 0 0 0 1px rgba(15, 26, 51, 0.06);
  overflow: hidden;
}
.tn-meter__fill {
  display: block;
  height: 100%;
  width: var(--tn-meter-fill, 0%);
  border-radius: inherit;
  background: var(--tn-color-action);
  transition: width var(--tn-duration-slow) var(--tn-easing-standard);
}
.tn-meter__fill[data-tone='approved'] { background: var(--tn-color-status-approved); }
.tn-meter__fill[data-tone='pending'] { background: var(--tn-color-status-pending); }
.tn-meter__fill[data-tone='rejected'] { background: var(--tn-color-status-rejected); }
.tn-meter__fill[data-tone='unsurveyed'] { background: var(--tn-color-status-unsurveyed); }

/*
 * A SWATCH IS NEVER AN INLINE COLOUR. Same root cause as the meter: four
 * legends across the field map and the classification map painted their keys
 * with style={{ background: '#c8362b' }} and rendered as EMPTY OUTLINES in
 * production while the map beside them was painted correctly by MapLibre from
 * JS. The key and the map disagreed on the screen the department projects in
 * review meetings. A status is one of four known values, so it is an attribute
 * and a rule, and there is nothing left for a policy to drop.
 */
.tn-swatch {
  display: inline-block;
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: var(--tn-radius-sm);
  background: var(--tn-color-neutral-soft);
  box-shadow: inset 0 0 0 1px rgba(15, 26, 51, 0.18);
}
.tn-swatch--dot { border-radius: 50%; }
.tn-swatch[data-status='unsurveyed'] { background: var(--tn-color-status-unsurveyed); }
.tn-swatch[data-status='pending'] { background: var(--tn-color-status-pending); }
.tn-swatch[data-status='approved'] { background: var(--tn-color-status-approved); }
.tn-swatch[data-status='rejected'] { background: var(--tn-color-status-rejected); }
.tn-swatch[data-status='draft'] { background: var(--tn-color-action); }

.tn-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* A visually hidden element that is focusable must become visible when it is
   focused, or a keyboard user is taken somewhere they cannot see. */
.tn-vh--focusable:focus-within {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip-path: none;
  white-space: normal;
}


/*
 * THE CARD IS THE WHOLE SHAPE LANGUAGE IN FOUR LINES.
 *
 * It was a 1px #aab2d2 box with a 10px radius: a drawn outline, which is what
 * every screen on this platform looked like it was made of. It is now a lit
 * surface — a 16px radius, a hairline so faint it reads as an edge rather than
 * as a line, and the two-layer elevation from the tokens. Hard borders are kept
 * only where they carry meaning: a row divider, and a control edge that has to
 * clear 3:1.
 */
.tn-card {
  background: var(--tn-color-surface-raised);
  border: 1px solid var(--tn-color-border);
  border-radius: var(--tn-radius-lg);
  box-shadow: var(--tn-shadow-sm);
  padding: var(--tn-space-lg);
}
.tn-card--flush { padding: 0; overflow: hidden; }
.tn-stack { display: flex; flex-direction: column; gap: var(--tn-space-md); }
.tn-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--tn-space-xs); }


.tn-skip {
  position: fixed;
  top: var(--tn-space-xs);
  left: var(--tn-space-xs);
  z-index: var(--tn-z-skip-link);
  padding: var(--tn-space-sm) var(--tn-space-md);
  background: var(--tn-color-surface-raised);
  color: var(--tn-color-text);
  border: 2px solid var(--tn-color-accent);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-lg);
  font-weight: var(--tn-weight-semibold);
  transform: translateY(-200%);
}
.tn-skip:focus { transform: translateY(0); }

/*
 * --tn-chrome-top is how many pixels of FIXED chrome sit above the content: the
 * masthead on a phone, the top bar on a desktop, plus the environment banner
 * when there is one. An app sizing a full-height region -- the surveyor's map --
 * subtracts it rather than hard-coding a number that is wrong in production,
 * where the banner does not exist.
 */
.tn-shell {
  --tn-banner-height: 0px;
  /*
   * The 1px is the header's own bottom border, and it is not pedantry: this
   * variable is what .fld-syncbar sticks to and what the field map subtracts
   * from the viewport, so a header measured 64px when it occupies 65 puts the
   * sticky strip one pixel high and the map one pixel long. Measured 7 Sep 2026
   * at 390x844: header box 0..65.
   */
  --tn-chrome-top: calc(var(--tn-size-header-height) + 1px + var(--tn-banner-height));
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
.tn-shell--banner { --tn-banner-height: 30px; }
/* On a phone the banner's sentence takes two lines (measured 50px at 390px),
   and the field map, sized from --tn-chrome-top, ran 20px under the bottom bar
   with the Google logo behind it. The banner is held to the height the
   variable says, so the two can never disagree. */
@media (max-width: 639.98px) {
  .tn-shell--banner { --tn-banner-height: 50px; }
}

/*
 * BETWEEN THE PHONE AND THE RAIL, THE HEADER IS TALLER THAN THE MASTHEAD.
 *
 * --tn-chrome-top means "how much is above main", and above the phone
 * breakpoint the section tabs are INSIDE .tn-shell__header (see AppShell), so
 * the header is the masthead plus a 44px row of tabs. Below sm the tabs are the
 * fixed bottom bar and the header really is just the masthead; at lg and up the
 * masthead is gone and the block further down replaces this with the topbar's
 * 56px. Only this middle band was wrong, and it was wrong by the height of a
 * touch target.
 *
 * What that broke is not cosmetic. .fld-syncbar pins itself with
 * top: var(--tn-chrome-top) and so sat 44px too high, sliding under the tabs
 * as the map scrolled; scroll-padding-top under-reserved by the same amount, so
 * an anchored jump landed with its heading behind the tabs; and the field map,
 * which now sizes itself from this variable, was 44px too tall. This is the
 * width band a 10in field tablet in landscape reports.
 */
@media (min-width: 640px) and (max-width: 1023.98px) {
  .tn-shell {
    --tn-chrome-top: calc(
      var(--tn-size-header-height) + var(--tn-size-touch) + 1px + var(--tn-banner-height)
    );
  }
}

.tn-shell__header {
  position: sticky;
  top: 0;
  z-index: var(--tn-z-header);
  background: var(--tn-color-header-bg);
  color: var(--tn-color-header-ink);
  border-bottom: 1px solid var(--tn-color-header-border);
}

.tn-env-banner {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tn-banner-height);
  padding: var(--tn-space-xxs) var(--tn-space-md);
  background: var(--tn-color-warning);
  color: var(--tn-color-warning-ink);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  letter-spacing: var(--tn-tracking-wider);
  text-align: center;
  text-transform: uppercase;
}

.tn-masthead {
  display: flex;
  align-items: center;
  gap: var(--tn-space-md);
  min-height: var(--tn-size-header-height);
  max-width: var(--tn-size-content-max);
  margin: 0 auto;
  padding: var(--tn-space-xs) var(--tn-space-md);
  width: 100%;
}

/*
 * The Seal of Tamil Nadu, in the masthead and at the head of the desktop rail.
 *
 * "flex: none" because the identity block beside it is allowed to shrink and the
 * emblem is not: a state emblem squashed to an ellipse is worse than no emblem.
 * Fixed height with "width: auto" keeps its 1280x1405 aspect exactly; the file
 * is served at 175x192 so it stays crisp on a 2x phone screen at this size.
 */
.tn-masthead__emblem {
  flex: none;
  align-self: center;
  height: 40px;
  width: auto;
  object-fit: contain;
  display: block;
}

/*
 * The rail brand is a COLUMN flex container, and that is the whole story here.
 *
 * A column flex container defaults to align-items: stretch, so a child with no
 * align-self is pulled to the container's full width -- 264px of rail. With
 * height pinned at 36px the state emblem became a 264x36 ellipse. "width: auto"
 * does not prevent it: stretch is a cross-axis instruction and beats an auto
 * width outright.
 *
 * align-self: flex-start opts this one child out, and only then does width:auto
 * mean what it reads like -- the intrinsic 175:192 ratio at the given height.
 * object-fit is belt and braces for the day someone sets both dimensions.
 */
.tn-side__emblem {
  flex: none;
  align-self: flex-start;
  height: 36px;
  width: auto;
  object-fit: contain;
  display: block;
  margin-bottom: var(--tn-space-xs);
}

/* On a phone the masthead is the only chrome and the service name needs the
   room more than the emblem does. */
@media (max-width: 639.98px) {
  .tn-masthead__emblem { height: 32px; }
}

.tn-masthead__identity { display: flex; flex-direction: column; min-width: 0; }
.tn-masthead__service {
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-medium);
  letter-spacing: var(--tn-tracking-wide);
  text-transform: uppercase;
  color: var(--tn-color-header-ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* On a phone the masthead has to hold the app name, who is signed in, the sync
   state and sign-out on 360px. The department line is the one thing there that
   a surveyor already knows and never acts on, so it is what goes: it wrapped to
   two lines and pushed everything else into a second row. The app name stays,
   because two of these apps look alike at a glance. */
@media (max-width: 639.98px) {
  .tn-masthead { gap: var(--tn-space-xs); padding-left: var(--tn-space-sm); padding-right: var(--tn-space-sm); }
  .tn-masthead__service { display: none; }
  .tn-masthead__side { gap: var(--tn-space-xs); }
}
.tn-masthead__app {
  font-size: var(--tn-text-lg);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-header-ink);
  line-height: var(--tn-leading-tight);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tn-masthead__spacer { flex: 1 1 auto; }

.tn-masthead__side {
  display: flex;
  align-items: center;
  gap: var(--tn-space-sm);
}

.tn-masthead__user {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--tn-space-xs);
  line-height: var(--tn-leading-tight);
  min-width: 0;
}
.tn-masthead__user-lines {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  min-width: 0;
  /* The two lines are set at leading-tight, which makes a line box shorter than
     the face's own ascent plus descent, so the name's descenders and the role's
     ascenders shared 3px of the same band. Two pixels of gap is the whole fix
     and it is invisible otherwise. */
  gap: 2px;
}
/* Initials on a ring rather than a filled disc: the masthead is already a
   solid colour, and a second solid shape on it reads as a hole. */
.tn-masthead__avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1.5px solid var(--tn-color-header-ink-muted);
  color: var(--tn-color-header-ink);
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  letter-spacing: var(--tn-tracking-wide);
}
/* The phone masthead is already tight; the name, the role and the avatar come
   back above the phone breakpoint. With the name and role hidden the avatar is
   a circle with a letter in it taking 42px out of a masthead that has none to
   spare. Three separate queries at 479/480/559 used to do this job, which is
   exactly the drift the one breakpoint scale was added to stop. */
@media (max-width: 639.98px) {
  .tn-masthead__user-lines { display: none; }
  .tn-masthead__avatar { display: none; }
}
.tn-masthead__user-name {
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-header-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 22ch;
}
.tn-masthead__user-role {
  font-size: var(--tn-text-xs);
  color: var(--tn-color-header-ink-muted);
}

.tn-nav { background: var(--tn-color-header-bg); }
/*
 * min-width:0 is the fix for the bar overflowing ITSELF by 4px at 360 — the
 * list is a flex line whose automatic minimum size was its content's, so the
 * box measured 364px inside 360px and the last item was clipped rather than
 * scrolled to. The scroller is also focusable (see AppShell.tsx): a region that
 * scrolls and cannot be reached by keyboard fails WCAG 2.1.1.
 */
.tn-nav__list {
  display: flex;
  gap: var(--tn-space-xxs);
  list-style: none;
  min-width: 0;
  max-width: var(--tn-size-content-max);
  margin: 0 auto;
  padding: 0 var(--tn-space-xs);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
.tn-nav__list:focus-visible { outline-offset: -3px; }
/* Everything focusable on the navy chrome rings in white: the page's focus
   blue is 1.77:1 on the header, and a ring there must reach 3:1. */
.tn-nav :focus-visible, .tn-shell__header :focus-visible { outline-color: var(--tn-color-header-ink); }
.tn-nav__link {
  display: flex;
  align-items: center;
  gap: var(--tn-space-xs);
  min-height: var(--tn-size-touch);
  padding: 0 var(--tn-space-md);
  color: var(--tn-color-header-ink-muted);
  text-decoration: none;
  font-size: var(--tn-text-base);
  font-weight: var(--tn-weight-medium);
  white-space: nowrap;
  /* The current tab is marked by a 3px bar, not by colour alone. */
  border-bottom: 3px solid transparent;
}
.tn-nav__link:hover { color: var(--tn-color-header-ink); background: rgba(255, 255, 255, 0.06); }
.tn-nav__link--current {
  color: var(--tn-color-header-ink);
  border-bottom-color: var(--tn-color-header-ink);
  font-weight: var(--tn-weight-semibold);
}
/* Stacks above the label in the phone bottom bar; sits beside it on desktop. */
.tn-nav__icon { display: inline-flex; line-height: 1; }

.tn-nav__badge {
  min-width: 22px;
  padding: 0 6px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-color-header-ink);
  color: var(--tn-color-header-bg);
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-bold);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.tn-shell__main {
  flex: 1 1 auto;
  width: 100%;
  max-width: var(--tn-size-content-max);
  margin: 0 auto;
  padding: var(--tn-space-xl) var(--tn-space-md) var(--tn-space-page);
  /*
   * THE FOOTER STARTS BELOW THE FOLD, ALWAYS. Every layout shift measured on the
   * platform (0.054 on /survey/queue, 0.167 on a capture form, 0.040 on
   * /review) was the footer jumping when the streamed page replaced its short
   * loading skeleton. With main at least a viewport tall, the footer is off
   * screen before and after the swap, and a shift nobody can see is not one.
   */
  min-height: calc(100dvh - var(--tn-chrome-top));
}
/* main is a focus target for the skip link; it must not draw a ring when the
   click that focused it came from a mouse. */
.tn-shell__main:focus { outline: none; }


/* ==========================================================================
   THE SIDEBAR — desktop only (>=1024px).
   Below that it is display:none and the masthead + fixed bottom bar take over,
   which is the layout a surveyor holds in one hand. Exactly one of the two is
   ever visible; see the note in AppShell.tsx for why both are in the markup.
   ========================================================================== */
.tn-side { display: none; }
.tn-topbar { display: none; }

@media (min-width: 1024px) {
  .tn-shell {
    display: grid;
    grid-template-columns: 264px minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    grid-template-rows: auto auto 1fr auto;
    grid-template-areas:
      'banner banner'
      'side   topbar'
      'side   main'
      'side   foot';
  }

  /* Spans both columns at the very top; the rail starts beneath it. */
  .tn-env-banner { grid-area: banner; }

  .tn-side {
    grid-area: side;
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100dvh;
    align-self: start;
    background: var(--tn-color-header-bg);
    color: var(--tn-color-header-ink);
    border-right: 1px solid var(--tn-color-header-border);
  }

  .tn-side__brand {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--tn-space-md) var(--tn-space-md) var(--tn-space-sm);
    border-bottom: 1px solid var(--tn-color-header-border);
  }
  .tn-side__service {
    font-size: var(--tn-text-xs);
    font-weight: var(--tn-weight-medium);
    letter-spacing: var(--tn-tracking-wider);
    text-transform: uppercase;
    color: var(--tn-color-header-ink-muted);
  }
  .tn-side__app {
    font-size: var(--tn-text-lg);
    font-weight: var(--tn-weight-semibold);
    line-height: var(--tn-leading-tight);
  }

  /* The rail scrolls, the brand and the user block do not: on a nine-item nav
     the account you are signed in as must not scroll out of sight. */
  .tn-side__nav {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--tn-space-sm) var(--tn-space-xs);
  }
  .tn-side__group + .tn-side__group { margin-top: var(--tn-space-md); }
  .tn-side__group-label {
    margin: 0 0 var(--tn-space-xxs);
    padding: 0 var(--tn-space-sm);
    font-size: var(--tn-text-xs);
    font-weight: var(--tn-weight-semibold);
    letter-spacing: var(--tn-tracking-wider);
    text-transform: uppercase;
    color: var(--tn-color-header-ink-muted);
  }
  .tn-side__list { list-style: none; margin: 0; padding: 0; }

  .tn-side__link {
    display: flex;
    align-items: center;
    gap: var(--tn-space-sm);
    min-height: var(--tn-size-touch);
    margin-bottom: 2px;
    padding: 0 var(--tn-space-sm);
    border-radius: var(--tn-radius-md);
    color: var(--tn-color-header-ink-muted);
    text-decoration: none;
    font-weight: var(--tn-weight-medium);
  }
  .tn-side__link:hover {
    background: color-mix(in srgb, var(--tn-color-header-ink) 10%, transparent);
    color: var(--tn-color-header-ink);
  }
  /* The current item is filled, not underlined. On a vertical rail an
     underline reads as a divider between two items rather than a state. */
  .tn-side__link--current {
    background: color-mix(in srgb, var(--tn-color-header-ink) 16%, transparent);
    color: var(--tn-color-header-ink);
    font-weight: var(--tn-weight-semibold);
  }
  /* White, not the page's focus blue: #1e40af on the navy rail is 1.77:1,
     under the 3:1 a focus indicator needs. White on navy is 15.47:1. */
  .tn-side__link:focus-visible {
    outline: 2px solid var(--tn-color-header-ink);
    outline-offset: -2px;
  }
  .tn-side__icon { display: inline-flex; flex: none; line-height: 1; }
  .tn-side__label { flex: 1 1 auto; min-width: 0; }
  .tn-side__badge {
    flex: none;
    min-width: 22px;
    padding: 0 6px;
    border-radius: var(--tn-radius-pill);
    background: var(--tn-color-header-ink);
    color: var(--tn-color-header-bg);
    font-size: var(--tn-text-xs);
    font-weight: var(--tn-weight-semibold);
    text-align: center;
  }

  .tn-side__foot {
    border-top: 1px solid var(--tn-color-header-border);
    padding: var(--tn-space-sm) var(--tn-space-md) var(--tn-space-md);
    display: flex;
    flex-direction: column;
    gap: var(--tn-space-sm);
  }
  .tn-side__user { display: flex; align-items: center; gap: var(--tn-space-xs); min-width: 0; }
  .tn-side__avatar {
    flex: none;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 1.5px solid var(--tn-color-header-ink-muted);
    font-size: var(--tn-text-xs);
    font-weight: var(--tn-weight-semibold);
    letter-spacing: var(--tn-tracking-wide);
  }
  .tn-side__user-lines { display: flex; flex-direction: column; min-width: 0; }
  .tn-side__user-name {
    font-size: var(--tn-text-sm);
    font-weight: var(--tn-weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .tn-side__user-role { font-size: var(--tn-text-xs); color: var(--tn-color-header-ink-muted); }
  /* There is no .tn-side__footer any more. The department line belongs to the
     page footer, which is the one that survives to print; see AppShell.tsx. */

  /* With the rail carrying identity and navigation, the masthead has nothing
     left to say — the page's own <PageHeader> names the screen. */
  .tn-shell__header { display: none; }

  /* The rail replaces the masthead, so above the content there is only the
     top bar (and the banner, if any). */
  .tn-shell { --tn-chrome-top: calc(56px + var(--tn-banner-height)); }

  .tn-topbar {
    grid-area: topbar;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--tn-space-sm);
    padding: var(--tn-space-xs) var(--tn-space-lg);
    min-height: 56px;
    background: var(--tn-color-surface);
    border-bottom: 1px solid var(--tn-color-border);
    position: sticky;
    top: 0;
    z-index: var(--tn-z-header);
  }

  /*
   * NO SECOND CAP. The main column centres itself inside 1280px, which is
   * right when the shell is a full-width masthead over a single column. With
   * the rail taking the left 264px it double-capped everything: a form already
   * limited to a sensible measure was then centred inside a narrower box, and
   * the leftover showed as a band of empty page down the right of every screen.
   *
   * The page decides its own measure now -- the map takes the lot, the form
   * caps itself in columns -- and the shell only supplies the gutters.
   */
  .tn-shell__main {
    grid-area: main;
    max-width: none;
    margin: 0;
    padding: var(--tn-space-lg) var(--tn-space-lg) var(--tn-space-page);
  }
  .tn-shell__footer { grid-area: foot; }
}

.tn-shell__footer {
  border-top: 1px solid var(--tn-color-border);
  background: transparent;
  color: var(--tn-color-text-muted);
  font-size: var(--tn-text-sm);
}
.tn-shell__footer-inner {
  max-width: var(--tn-size-content-max);
  margin: 0 auto;
  padding: var(--tn-space-lg) var(--tn-space-md);
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-sm) var(--tn-space-xl);
  align-items: center;
  justify-content: space-between;
}

/* Phone. Navigation moves to the bottom of the screen, inside the thumb arc of
   a hand that is also holding a torch or a tape. The header keeps the identity
   and the signed-in user; it loses the tabs. */
@media (max-width: 639.98px) {
  .tn-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: var(--tn-z-bottom-nav);
    border-top: 1px solid var(--tn-color-header-border);
    /* Clear of the iOS home indicator and Android gesture bar. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .tn-nav__list { padding: 0; }
  .tn-nav__link {
    flex: 1 0 auto;
    justify-content: center;
    flex-direction: column;
    gap: 2px;
    min-height: var(--tn-size-bottom-nav-height);
    padding: 0 var(--tn-space-sm);
    font-size: var(--tn-text-xs);
    border-bottom: none;
    border-top: 3px solid transparent;
  }
  .tn-nav__link--current { border-top-color: var(--tn-color-header-ink); }
  .tn-shell__main {
    padding: var(--tn-space-md) var(--tn-space-sm);
  }
  /*
   * THE FOOTER RESERVES THE BAR TOO, AND THAT WAS THE WHOLE BUG.
   *
   * main reserved bottom-nav-height + xxl and the footer did not — and the
   * footer is main's SIBLING, not its child, so main's padding did nothing for
   * it. Scrolled to the end of /survey, /survey/queue, /review, /review/qc,
   * /review/assignments, /classify, /admin or /admin/users, 45px of the footer
   * sat under the fixed bar. The line it hid was the audit-trail notice, on a
   * platform whose charter is that every change is attributable.
   */
  .tn-shell__footer {
    /* The bar is bottom-nav-height PLUS its own 1px top border, and a footer
       that clears it by exactly zero clears it by minus one after rounding.
       The xs step is the margin. */
    padding-bottom: calc(
      var(--tn-size-bottom-nav-height) + var(--tn-space-xs) + env(safe-area-inset-bottom, 0px)
    );
  }
  .tn-masthead__user-name { max-width: 12ch; }
  /*
   * At 360px the app name, the actions and a text button 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.
   * These lived in the staff field stylesheet, which used to be linked on every
   * page; they are the shell's, so they are here, after the base masthead
   * rules they override.
   */
  .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); }
}


.tn-page-header { margin-bottom: var(--tn-space-xl); }

.tn-page-header__breadcrumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tn-space-xxs);
  list-style: none;
  margin-bottom: var(--tn-space-xs);
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}
.tn-page-header__crumb { display: flex; align-items: center; gap: var(--tn-space-xxs); }
.tn-page-header__crumb-sep { color: var(--tn-color-text-faint); }
.tn-page-header__crumb a {
  color: var(--tn-color-text-muted);
  /* A breadcrumb is a link a person taps to go back, and at 22px tall it was
     one of the smallest targets on the platform. inline-flex plus a minimum
     height gives it a real hit area without changing where the text sits. */
  display: inline-flex;
  align-items: center;
  min-height: var(--tn-size-touch);
}
.tn-page-header__crumb a:hover { color: var(--tn-color-accent); }

/*
 * THE BACK CONTROL. A real target, above the trail.
 *
 * 44px tall and padded, because it is the control a surveyor reaches for with a
 * gloved thumb to leave a form. The breadcrumb link it supersedes measured 31px
 * wide on a 390px phone.
 *
 * On a phone this is the ONLY way back: the portal is installed to the home
 * screen, where there is no browser chrome and no browser back button.
 */
.tn-page-header__back {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-xxs);
  min-height: var(--tn-size-touch);
  margin-left: calc(var(--tn-space-xs) * -1);
  padding: 0 var(--tn-space-xs);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-color-accent);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  text-decoration: none;
}
.tn-page-header__back svg { width: 20px; height: 20px; flex: 0 0 auto; }
.tn-page-header__back:hover { background: var(--tn-color-surface-sunken); }
.tn-page-header__back:focus-visible {
  outline: 3px solid var(--tn-color-focus-ring);
  outline-offset: 2px;
}

/*
 * THE TRAIL IS FOR WIDE SCREENS ONLY, now that the button exists.
 *
 * Two controls saying "go back", stacked, on a 390px screen that is mostly map
 * and form, is a waste of the scarcest thing on the page. The trail says where
 * you ARE, which is worth the room on a desktop and is not worth it on a phone
 * where the title says the same thing one line below.
 */
@media (max-width: 639.98px) {
  .tn-page-header__breadcrumbs { display: none; }
}

/*
 * A BARE NUMBER THAT IS A LINK.
 *
 * On /review/assignments the count of records waiting on a feature class is an
 * anchor, and "3" is nine pixels wide. Measured at 9x25px, it was the smallest
 * target on the platform by a wide margin. A link whose whole text is a figure
 * gets a square target; the tabular figures keep the column aligned.
 */
a.tn-num {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  min-width: var(--tn-size-touch);
  min-height: var(--tn-size-touch);
}

.tn-page-header__eyebrow {
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  letter-spacing: var(--tn-tracking-wider);
  text-transform: uppercase;
  color: var(--tn-color-accent-soft-ink);
  margin-bottom: var(--tn-space-xs);
}

.tn-page-header__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--tn-space-md);
}

.tn-page-header__heading { display: flex; align-items: center; gap: var(--tn-space-sm); flex-wrap: wrap; min-width: 0; }
.tn-page-header__title {
  font-size: var(--tn-text-xxl);
  font-weight: var(--tn-weight-bold);
  letter-spacing: var(--tn-tracking-tight);
}
.tn-page-header__description {
  margin-top: var(--tn-space-sm);
  max-width: var(--tn-size-prose-max);
  color: var(--tn-color-text-muted);
  line-height: var(--tn-leading-relaxed);
}
.tn-page-header__actions { display: flex; flex-wrap: wrap; gap: var(--tn-space-xs); }

.tn-page-header__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-md) var(--tn-space-xxl);
  margin: var(--tn-space-md) 0 0;
  padding: var(--tn-space-sm) 0 0;
  border-top: 1px solid var(--tn-color-border);
}
.tn-page-header__meta-item { min-width: 0; }
.tn-page-header__meta-label {
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-text-faint);
}
.tn-page-header__meta-value {
  margin: 2px 0 0;
  font-size: var(--tn-text-base);
  font-weight: var(--tn-weight-medium);
}

@media (max-width: 639.98px) {
  .tn-page-header__title { font-size: var(--tn-text-xl); }
  .tn-page-header__actions { width: 100%; }
  .tn-page-header__actions > * { flex: 1 1 auto; }
}


.tn-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--tn-space-xs);
  min-height: var(--tn-size-control);
  padding: 0 var(--tn-space-md);
  border: 1px solid transparent;
  border-radius: var(--tn-radius-md);
  font-size: var(--tn-text-base);
  font-weight: var(--tn-weight-semibold);
  /* tight, not 1: a label that wraps to two lines must not collide. */
  line-height: var(--tn-leading-tight);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* A government form's submit button gets double-tapped constantly; text
     selection on the second tap looks like a bug. */
  user-select: none;
  transition: background-color var(--tn-duration-fast) var(--tn-easing-standard),
              border-color var(--tn-duration-fast) var(--tn-easing-standard),
              box-shadow var(--tn-duration-fast) var(--tn-easing-standard),
              color var(--tn-duration-fast) var(--tn-easing-standard);
}
.tn-btn:focus-visible { box-shadow: var(--tn-shadow-focus); }

.tn-btn--sm { min-height: var(--tn-size-control-compact); padding: 0 var(--tn-space-sm); font-size: var(--tn-text-sm); border-radius: var(--tn-radius-sm); }
/*
 * A COMPACT BUTTON IS A DESKTOP IDEA. Below the phone breakpoint it becomes a
 * real thumb target, and this is the rule that clears the bulk of the 87
 * sub-44px targets measured at 360 and 390: Start on every queue row, Cancel in
 * the arming banner, the two pack switches, Older events, Manage sites, The
 * role matrix, All sites and Sign out.
 *
 * IT LIVES HERE, NOT IN THE SHELL'S PHONE BLOCK, and that matters. The shell is
 * emitted before this file's BUTTON section, so a media rule there has the same
 * specificity as .tn-btn--sm and LOSES to it on source order — measured: the
 * masthead's Sign out stayed 36px at 390 with the rule in the shell and is 44px
 * with it here. A media query buys no priority.
 */
@media (max-width: 639.98px) {
  .tn-btn--sm { min-height: var(--tn-size-touch); }
}
/*
 * AND ON ANY DEVICE DRIVEN BY A FINGER, whatever its width. The rule above asks
 * how wide the screen is, which is the wrong question: the reason a control has
 * to be 44px is that a fingertip is about 44px, and a fingertip does not shrink
 * when the surveyor picks up a tablet. Field staff at Aliyar carry 8-10in
 * Android tablets in a waterproof case, which report 800-1280px wide and sail
 * straight past a max-width query — every compact button on those was 36px.
 *
 * pointer: coarse is the honest test. It is false for a mouse at any size, so
 * nothing on a desk grows, and true for touch at any size, which is exactly the
 * set of users the 44px minimum was written for.
 */
@media (pointer: coarse) {
  .tn-btn--sm { min-height: var(--tn-size-touch); }
}
.tn-btn--lg { min-height: var(--tn-size-control-lg); padding: 0 var(--tn-space-lg); font-size: var(--tn-text-md); border-radius: var(--tn-radius-lg); }
.tn-btn--block { display: flex; width: 100%; }

/* A filled action gets a contact shadow rather than a darker edge. It is the
   same idea as the cards: the thing is legible because it is lit, not because
   it is outlined, and it lifts the one control on a screen that should be
   obviously pressable above everything that merely looks like a box. */
/* Buttons read the ACTION family, not the accent: the page went white and dark
   blue on 28 Sep 2026 and the buttons were to keep their colours. */
.tn-btn--primary {
  background: var(--tn-color-action);
  color: var(--tn-color-action-ink);
  box-shadow: var(--tn-shadow-xs);
}
.tn-btn--primary:hover { background: var(--tn-color-action-hover); color: var(--tn-color-action-ink); box-shadow: var(--tn-shadow-sm); }
.tn-btn--primary:active { background: var(--tn-color-action-active); box-shadow: none; }

.tn-btn--secondary {
  background: var(--tn-color-surface-raised);
  color: var(--tn-color-text);
  border-color: var(--tn-color-border-control);
}
.tn-btn--secondary:hover { background: var(--tn-color-action-soft); color: var(--tn-color-action-soft-ink); border-color: var(--tn-color-action); }

.tn-btn--danger { background: var(--tn-color-danger); color: var(--tn-color-danger-ink); box-shadow: var(--tn-shadow-xs); }
.tn-btn--danger:hover { background: var(--tn-color-danger-hover); color: var(--tn-color-danger-ink); }

.tn-btn--ghost { background: transparent; color: var(--tn-color-action); }
.tn-btn--ghost:hover { background: var(--tn-color-action-soft); color: var(--tn-color-action-soft-ink); }

/*
 * A ghost button ON THE NAVY MASTHEAD needs the header's ink, not the accent.
 * The accent is #2b3fd0 on the header's #17205a: 1.96:1, which is not a
 * low-contrast button but an invisible one — and below 1024px the masthead is
 * the only chrome on screen, so the button it was hiding is sign-out. Header
 * ink gives 15.10:1, and the hairline (45% ink over the header, #7f84a4) gives
 * 4.13:1, so it still reads as a control rather than as a stray label.
 *
 * Scoped to .tn-masthead rather than fixed on --tn-color-accent: the same
 * variant on a light card and in the desktop top bar is correct as it is, and
 * an app that patched this with an inline style is now patching it on both.
 */
.tn-masthead .tn-btn--ghost {
  color: var(--tn-color-header-ink);
  border-color: color-mix(in srgb, var(--tn-color-header-ink) 45%, transparent);
}
.tn-masthead .tn-btn--ghost:hover {
  background: color-mix(in srgb, var(--tn-color-header-ink) 14%, transparent);
  color: var(--tn-color-header-ink);
}

/* Busy is a distinct state from disabled, and it is excluded from the disabled
   styling below on purpose: a submitting button is still the same button, and
   greying it out mid-request makes the page look like it lost the action. */
.tn-btn[disabled]:not([aria-busy='true']),
.tn-btn[aria-disabled='true']:not([aria-busy='true']) {
  cursor: not-allowed;
  background: var(--tn-color-surface-sunken);
  color: var(--tn-color-text-faint);
  border-color: var(--tn-color-border);
  /* No opacity trick: a 50%-opaque label on a light card drops below 4.5:1,
     and "disabled" is exactly the state a user needs to be able to read. */
}

.tn-btn[aria-busy='true'] { cursor: progress; }

.tn-btn__spinner {
  width: 16px;
  height: 16px;
  flex: none;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: tn-spin 700ms linear infinite;
}
.tn-btn__icon { display: inline-flex; flex: none; }
/* A block button on a 360px screen must let its label shrink rather than push
   the spinner off the end. */
.tn-btn__label { min-width: 0; }

@keyframes tn-spin { to { transform: rotate(360deg); } }


.tn-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-xxs);
  padding: 3px var(--tn-space-xs);
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  line-height: var(--tn-leading-snug);
  white-space: nowrap;
}
.tn-badge--sm { font-size: var(--tn-text-xs); padding: 1px 6px; }

.tn-badge--soft.tn-badge--neutral { background: var(--tn-color-neutral-soft); color: var(--tn-color-neutral-soft-ink); }
.tn-badge--soft.tn-badge--accent  { background: var(--tn-color-accent-soft);  color: var(--tn-color-accent-soft-ink); }
.tn-badge--soft.tn-badge--info    { background: var(--tn-color-accent-soft);  color: var(--tn-color-accent-soft-ink); }
.tn-badge--soft.tn-badge--success { background: var(--tn-color-success-soft); color: var(--tn-color-success-soft-ink); }
.tn-badge--soft.tn-badge--warning { background: var(--tn-color-warning-soft); color: var(--tn-color-warning-soft-ink); }
.tn-badge--soft.tn-badge--danger  { background: var(--tn-color-danger-soft);  color: var(--tn-color-danger-soft-ink); }

.tn-badge--solid.tn-badge--neutral { background: var(--tn-color-text-muted); color: var(--tn-color-surface-raised); }
.tn-badge--solid.tn-badge--accent,
.tn-badge--solid.tn-badge--info    { background: var(--tn-color-accent);  color: var(--tn-color-accent-ink); }
.tn-badge--solid.tn-badge--success { background: var(--tn-color-success); color: var(--tn-color-success-ink); }
.tn-badge--solid.tn-badge--warning { background: var(--tn-color-warning); color: var(--tn-color-warning-ink); }
.tn-badge--solid.tn-badge--danger  { background: var(--tn-color-danger);  color: var(--tn-color-danger-ink); }

.tn-badge--outline { background: transparent; border-color: var(--tn-color-border-control); color: var(--tn-color-text-muted); }
.tn-badge--outline.tn-badge--accent,
.tn-badge--outline.tn-badge--info    { border-color: var(--tn-color-accent);  color: var(--tn-color-accent); }
.tn-badge--outline.tn-badge--success { border-color: var(--tn-color-success); color: var(--tn-color-success-soft-ink); }
.tn-badge--outline.tn-badge--warning { border-color: var(--tn-color-warning); color: var(--tn-color-warning-soft-ink); }
.tn-badge--outline.tn-badge--danger  { border-color: var(--tn-color-danger);  color: var(--tn-color-danger-soft-ink); }

.tn-status {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-xs);
  padding: 3px var(--tn-space-sm) 3px var(--tn-space-xs);
  border-radius: var(--tn-radius-pill);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  line-height: var(--tn-leading-snug);
  white-space: nowrap;
}
.tn-status--md { font-size: var(--tn-text-base); padding: 5px var(--tn-space-md) 5px var(--tn-space-sm); }

/* The dot carries the same hex the map paints the polygon with; the tint and
   ink behind it are the accessible pair. That is how a pill and a footprint
   stay recognisably the same colour without the pill failing contrast. */
.tn-status__dot { width: 10px; height: 10px; flex: none; border-radius: 50%; }
.tn-status__count {
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
  font-weight: var(--tn-weight-medium);
}

.tn-status[data-status='unsurveyed'] { background: var(--tn-color-status-unsurveyed-soft); color: var(--tn-color-status-unsurveyed-ink); }
.tn-status[data-status='unsurveyed'] .tn-status__dot { background: var(--tn-color-status-unsurveyed); }
.tn-status[data-status='pending']    { background: var(--tn-color-status-pending-soft);    color: var(--tn-color-status-pending-ink); }
.tn-status[data-status='pending'] .tn-status__dot    { background: var(--tn-color-status-pending); }
.tn-status[data-status='approved']   { background: var(--tn-color-status-approved-soft);   color: var(--tn-color-status-approved-ink); }
.tn-status[data-status='approved'] .tn-status__dot   { background: var(--tn-color-status-approved); }
.tn-status[data-status='rejected']   { background: var(--tn-color-status-rejected-soft);   color: var(--tn-color-status-rejected-ink); }
.tn-status[data-status='rejected'] .tn-status__dot   { background: var(--tn-color-status-rejected); }


.tn-field { display: block; margin-bottom: var(--tn-space-lg); }

/*
 * The box that holds one question and its answer. See Field.tsx for why they
 * are boxed together rather than stacked as two objects.
 */
.tn-field__box {
  padding: var(--tn-space-xs) var(--tn-space-md) var(--tn-space-xs);
  background: var(--tn-color-surface-raised);
  /* borderControl, not borderStrong: this IS a control boundary, and WCAG
     1.4.11 asks 3:1 of one. borderStrong is a decorative hairline at 1.84:1
     and was carrying a job it cannot do. */
  border: 1px solid var(--tn-color-border-control);
  border-radius: var(--tn-radius-md);
  transition: border-color var(--tn-duration-fast) var(--tn-easing-standard),
              box-shadow var(--tn-duration-fast) var(--tn-easing-standard);
}
/* The ring goes on the BOX, not the input -- the input has no border of its
   own any more, so a ring drawn on it would float unattached inside the box. */
.tn-field__box:focus-within {
  border-color: var(--tn-color-accent);
  box-shadow: var(--tn-shadow-focus);
}

/* A caption inside its own box, set in the accent so the question reads as a
   heading for the answer beneath it. Sentence case at the secondary step, the
   convention of every desktop and phone platform: forty-seven uppercase
   captions on one capture screen read as shouting, and uppercase is slower to
   read for the same letters. */
.tn-field__label {
  display: block;
  margin-bottom: 2px;
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  line-height: var(--tn-leading-snug);
  color: var(--tn-color-accent-soft-ink);
}

/*
 * ONLY AN INVALID FIELD COLOURS ITS BOX.
 *
 * Required used to colour it too, which turned the sign-in page -- two fields,
 * both required -- entirely red before anyone had typed anything, so a page in
 * a perfectly ordinary state read as two validation errors. Danger is reserved
 * for a field that has actually been refused.
 *
 * This also matches what Field.tsx already says about the marker: "marking the
 * few exceptions is quieter than starring the rest". The asterisk carries
 * required; the border carries wrong.
 */
.tn-field--invalid .tn-field__box { border-color: var(--tn-color-danger); }
.tn-field--invalid .tn-field__box:focus-within {
  border-color: var(--tn-color-danger);
  box-shadow: 0 0 0 3px var(--tn-color-danger-soft);
}
.tn-field--invalid .tn-field__label { color: var(--tn-color-danger-soft-ink); }
.tn-field__required { color: var(--tn-color-danger-soft-ink); margin-left: 2px; }
.tn-field__optional {
  margin-left: var(--tn-space-xs);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-regular);
  color: var(--tn-color-text-faint);
}

/* Hints sit BELOW the control, not between the label and it. Above, every hint
   pushed the answer further from the question and a scan down the form hit
   label, prose, box, label, prose, box. Below, the column of controls lines up
   and the prose is there for whoever stops. */
.tn-field__hint {
  margin-top: var(--tn-space-xs);
  font-size: var(--tn-text-xs);
  line-height: var(--tn-leading-normal);
  color: var(--tn-color-text-faint);
  max-width: 52ch;
}

/*
 * The control is naked inside the box: no border, no ground of its own, no
 * padding that would indent the answer from its label. The box is the control
 * as far as a reader is concerned.
 *
 * min-height IS THE TARGET SIZE, and it used to be 26px. That is every form
 * control on the platform — the sign-in fields, the three review filters, all
 * of /admin/settings and /admin/activity, and every question on the capture
 * form. The box around it was taller, so the effective target was better than
 * 26px, but the box's own padding was dead space and the control a surveyor
 * aims at in the sun was 26 pixels of it. It is a full 44px target now, and
 * the box has grown by about 18px per field, which is most of the extra air on
 * every form on the site.
 */
.tn-field__control > input,
.tn-field__control > select,
.tn-field__control > textarea {
  display: block;
  width: 100%;
  min-height: var(--tn-size-control);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--tn-color-text);
  /* 16px floor on a phone, and not negotiable: iOS Safari zooms the viewport on
     focus for anything smaller, which throws away the map the surveyor was
     working in. The base step is 16px below 640px, 14px on a desk. */
  font-size: var(--tn-text-base);
}
.tn-field__control > textarea { min-height: 132px; padding: var(--tn-space-xs) 0; line-height: var(--tn-leading-normal); resize: vertical; }

/*
 * A SELECT NOW LOOKS LIKE A SELECT.
 *
 * appearance:none stripped the native chevron and 32px of padding-right was
 * reserved for a replacement that was never drawn — so every <select> on the
 * platform was indistinguishable from a text input, at all five widths, on the
 * review filters, the account pickers, /admin/settings and every choice field
 * on the capture form. A person cannot know a control is a menu until they tap
 * it.
 *
 * The chevron is an inline SVG data: URI in this stylesheet, not an <img> and
 * not an inline style: img-src allows data:, the rule ships in the same file as
 * everything else, and there is no request to fail. It is stroked in
 * currentColor's place with the muted ink so it reads at 4.5:1 on white.
 */
.tn-field__control > select {
  appearance: none;
  padding-right: var(--tn-space-xl);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none' stroke='%23434e68' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0 center;
  background-size: 20px 20px;
  cursor: pointer;
}
.tn-field__control > input[type='checkbox'],
.tn-field__control > input[type='radio'] {
  width: 24px;
  height: 24px;
  min-height: 0;
  accent-color: var(--tn-color-accent);
}
/*
 * A date input keeps its native picker button, which the min-height above would
 * otherwise stretch to 44px and leave floating. It also gets an explicit
 * min-width: Chrome collapses an empty date field to the width of its
 * placeholder and the field then does not fill its box.
 */
.tn-field__control > input[type='date'],
.tn-field__control > input[type='time'] { min-width: 0; }
.tn-field__control > input::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: 0.65;
  padding: var(--tn-space-xs);
}
.tn-field__control > input::-webkit-calendar-picker-indicator:hover { opacity: 1; }

.tn-field__control > input:disabled,
.tn-field__control > select:disabled,
.tn-field__control > textarea:disabled {
  background: var(--tn-color-surface-sunken);
  color: var(--tn-color-text-muted);
  cursor: not-allowed;
}
.tn-field__control > input[readonly],
.tn-field__control > textarea[readonly] {
  background: var(--tn-color-surface-sunken);
  border-style: dashed;
}

.tn-field--invalid .tn-field__control > input,
.tn-field--invalid .tn-field__control > select,
.tn-field--invalid .tn-field__control > textarea {
  border-color: var(--tn-color-danger);
  /* Two carriers, not one: a thicker edge as well as a red one, so a red-green
     colour deficiency does not hide the invalid field. */
  border-width: 2px;
}

.tn-field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-xxs);
  margin-top: var(--tn-space-xs);
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-medium);
  color: var(--tn-color-danger-soft-ink);
}
.tn-field__error-mark { flex: none; font-weight: var(--tn-weight-bold); }

.tn-field__counter {
  margin-top: var(--tn-space-xxs);
  font-size: var(--tn-text-xs);
  color: var(--tn-color-text-faint);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Checkbox and radio: label beside the control, whole row a 44px target. */
.tn-field--inline { display: flex; align-items: flex-start; gap: var(--tn-space-sm); }
.tn-field--inline .tn-field__control { order: -1; padding-top: 2px; }
.tn-field--inline .tn-field__label { margin-bottom: 0; font-weight: var(--tn-weight-regular); }
.tn-field--inline .tn-field__body { min-height: var(--tn-size-touch); display: flex; flex-direction: column; justify-content: center; }


.tn-table-wrap {
  background: var(--tn-color-surface-raised);
  border: 1px solid var(--tn-color-border);
  border-radius: var(--tn-radius-lg);
  box-shadow: var(--tn-shadow-sm);
  overflow: hidden;
}
/*
 * The horizontal scroll lives on the table's own container, never on the page.
 * tabindex makes it reachable by keyboard, which a scrollable region needs.
 *
 * The right-hand fade is a scroll AFFORDANCE, and it is not decoration: a table
 * clipped at exactly the container edge gives a reader no signal that there are
 * more columns, which is how the eleventh column of the role matrix went
 * unnoticed. It is painted with two background layers — a white "cover" pinned
 * to the container and a gradient pinned to the content - so it is visible only
 * while there is something still to scroll to, and disappears at the end.
 */
.tn-table-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  background:
    linear-gradient(to right, var(--tn-color-surface-raised) 30%, rgba(255, 255, 255, 0)) left center / 40px 100% no-repeat,
    linear-gradient(to left, var(--tn-color-surface-raised) 30%, rgba(255, 255, 255, 0)) right center / 40px 100% no-repeat,
    linear-gradient(to right, rgba(12, 38, 33, 0.12), rgba(255, 255, 255, 0)) left center / 14px 100% no-repeat,
    linear-gradient(to left, rgba(12, 38, 33, 0.12), rgba(255, 255, 255, 0)) right center / 14px 100% no-repeat;
  background-attachment: local, local, scroll, scroll;
}
.tn-table-scroll:focus-visible { outline-offset: -3px; }

/* Desktop tables are dense: 13px cells, 40px rows, sentence-case heads. A phone
   gets the stacked card layout at the body size (see the block at the end). */
.tn-table { width: 100%; border-collapse: collapse; font-size: var(--tn-text-sm); line-height: var(--tn-leading-snug); }

.tn-table__caption {
  padding: var(--tn-space-md);
  text-align: left;
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-text-muted);
  background: var(--tn-color-surface-sunken);
  border-bottom: 1px solid var(--tn-color-border);
}

.tn-table th {
  padding: var(--tn-space-xs) var(--tn-space-sm);
  text-align: left;
  font-size: var(--tn-text-sm);
  font-weight: var(--tn-weight-semibold);
  color: var(--tn-color-text-muted);
  background: var(--tn-color-surface-sunken);
  border-bottom: 1px solid var(--tn-color-border);
  white-space: nowrap;
}
.tn-table--sticky th { position: sticky; top: 0; z-index: var(--tn-z-sticky); }

.tn-table td {
  /* A table cell's height is its minimum: every row is at least 40px. */
  height: 40px;
  padding: var(--tn-space-xs) var(--tn-space-sm);
  border-bottom: 1px solid var(--tn-color-border);
  vertical-align: middle;
}
.tn-table--compact th, .tn-table--compact td { padding: var(--tn-space-xs) var(--tn-space-sm); }
.tn-table tbody tr:last-child td { border-bottom: none; }
.tn-table tbody tr:hover { background: var(--tn-color-accent-soft); }

.tn-table [data-align='end'] { text-align: right; }
.tn-table [data-align='center'] { text-align: center; }
.tn-table [data-numeric='true'] { font-variant-numeric: tabular-nums lining-nums; }

.tn-table__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-xxs);
  min-height: var(--tn-size-control-compact);
  color: inherit;
  text-decoration: none;
  font-weight: inherit;
}
.tn-table__sort:hover { color: var(--tn-color-accent); }
.tn-table__sort-icon { font-size: var(--tn-text-xs); opacity: 0.75; }

/* A cell link is a 44px row target on a phone, not a 25px line of text. The
   inline-flex plus min-height is what lifts .tn-table__cell-link off the
   sub-target list without changing the look of a desktop table row. */
.tn-table__cell-link {
  display: inline-flex;
  align-items: center;
  /* 32px under a mouse, lifted to 44px on a phone or under a finger by the
     token itself. */
  min-height: var(--tn-size-control-compact);
  font-weight: var(--tn-weight-semibold);
}
/* The whole row is the target on a phone, so the link's own padding is
   stretched over the cell rather than the row being given a click handler. A
   real anchor keeps middle-click, "open in new tab" and the status bar URL. */
.tn-table__cell-link::after { content: ''; }

.tn-table__footnote {
  padding: var(--tn-space-sm) var(--tn-space-md);
  border-top: 1px solid var(--tn-color-border);
  background: var(--tn-color-surface-sunken);
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}

.tn-table__state { padding: var(--tn-space-xxl) var(--tn-space-md); }

/* Phone: a 9-column table is unreadable at 360px and horizontal scrolling
   loses the header. Each row becomes a card, each cell keeps its column name
   in front of it, and columns marked secondary drop out entirely — they are
   still on the record page, which is one tap away. */
@media (max-width: 639.98px) {
  .tn-table { font-size: var(--tn-text-base); }
  .tn-table, .tn-table tbody, .tn-table tr, .tn-table td { display: block; width: 100%; }
  .tn-table thead { display: none; }
  .tn-table tr {
    border-bottom: 1px solid var(--tn-color-border);
    padding: var(--tn-space-sm) 0;
  }
  .tn-table tbody tr:last-child { border-bottom: none; }
  .tn-table td {
    display: flex;
    gap: var(--tn-space-md);
    justify-content: space-between;
    align-items: baseline;
    border-bottom: none;
    height: auto;
    padding: var(--tn-space-xxs) var(--tn-space-md);
    text-align: left;
  }
  .tn-table td::before {
    content: attr(data-label);
    flex: 0 0 38%;
    font-size: var(--tn-text-xs);
    font-weight: var(--tn-weight-semibold);
    color: var(--tn-color-text-muted);
  }
  /* The fade affordance is for a horizontal scroller. In card mode there is no
     horizontal scroll and the gradient just tints the first and last column. */
  .tn-table-scroll { background: none; }
  .tn-table td[data-priority='secondary'] { display: none; }
  /* The empty/error/loading row is not a field, so it gets no column name in
     front of it and no 40% gutter to sit in. */
  .tn-table td.tn-table__state { display: block; }
  .tn-table td.tn-table__state::before { content: none; }
  .tn-table [data-align='end'], .tn-table [data-align='center'] { text-align: left; }
}


.tn-empty, .tn-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--tn-space-sm);
  padding: var(--tn-space-section) var(--tn-space-lg);
  text-align: center;
}
.tn-empty--sm, .tn-error--sm { padding: var(--tn-space-xl) var(--tn-space-md); }

.tn-empty__icon { color: var(--tn-color-text-faint); }
.tn-empty__title { font-size: var(--tn-text-lg); font-weight: var(--tn-weight-semibold); }
.tn-empty__body { max-width: 52ch; color: var(--tn-color-text-muted); }
.tn-empty__action { margin-top: var(--tn-space-xs); display: flex; gap: var(--tn-space-xs); flex-wrap: wrap; justify-content: center; }

/* ErrorState is not decorated like a 404 illustration. It is a report: what
   failed, what the reader can do about it, and the request id that ties it to
   the log line an administrator will search for. Never a stack trace — the
   detail belongs in the structured log, not on a government user's screen. */
.tn-error {
  border: 1px solid var(--tn-color-danger);
  border-left-width: 4px;
  border-radius: var(--tn-radius-lg);
  background: var(--tn-color-surface-raised);
  box-shadow: var(--tn-shadow-sm);
  align-items: flex-start;
  text-align: left;
}
.tn-error--warning { border-color: var(--tn-color-warning); }

.tn-error__head { display: flex; align-items: flex-start; gap: var(--tn-space-sm); }
.tn-error__icon { flex: none; color: var(--tn-color-danger-soft-ink); }
.tn-error--warning .tn-error__icon { color: var(--tn-color-warning-soft-ink); }
.tn-error__title { font-size: var(--tn-text-lg); font-weight: var(--tn-weight-semibold); }
.tn-error__what { max-width: var(--tn-size-prose-max); }
.tn-error__hint { max-width: var(--tn-size-prose-max); color: var(--tn-color-text-muted); }

.tn-error__ref {
  margin-top: var(--tn-space-xxs);
  padding: var(--tn-space-xs) var(--tn-space-sm);
  background: var(--tn-color-surface-sunken);
  border-radius: var(--tn-radius-sm);
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text-muted);
}
.tn-error__ref code {
  font-family: var(--tn-font-mono);
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text);
  user-select: all;
}
.tn-error__actions { display: flex; gap: var(--tn-space-xs); flex-wrap: wrap; margin-top: var(--tn-space-xs); }
.tn-error__partial { width: 100%; margin-top: var(--tn-space-md); padding-top: var(--tn-space-md); border-top: 1px solid var(--tn-color-border); }

.tn-skeleton {
  display: block;
  background: linear-gradient(
    90deg,
    var(--tn-color-skeleton-base) 25%,
    var(--tn-color-skeleton-sheen) 37%,
    var(--tn-color-skeleton-base) 63%
  );
  background-size: 400% 100%;
  animation: tn-shimmer 1400ms ease-in-out infinite;
  border-radius: var(--tn-radius-sm);
}
.tn-skeleton--text { height: 1em; margin: 0.28em 0; border-radius: var(--tn-radius-sm); }
.tn-skeleton--text:last-child { width: 72%; }
.tn-skeleton--block { border-radius: var(--tn-radius-md); }
.tn-skeleton--circle { border-radius: 50%; }
.tn-skeleton-group { display: block; }

@keyframes tn-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }


.tn-toast-viewport {
  position: fixed;
  z-index: var(--tn-z-toast);
  bottom: calc(var(--tn-space-md) + env(safe-area-inset-bottom, 0px));
  right: var(--tn-space-md);
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-xs);
  width: min(420px, calc(100vw - var(--tn-space-xl)));
  /* The viewport spans a region but must not swallow clicks on the page under
     it; only the toasts themselves are interactive. */
  pointer-events: none;
}
/* Two lists, one assertive and one polite. Both are always in the DOM so a
   screen reader has a region to announce into; neither shows a marker. */
.tn-toast-viewport ol { list-style: none; }

.tn-toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-sm);
  padding: var(--tn-space-sm) var(--tn-space-sm) var(--tn-space-sm) var(--tn-space-md);
  background: var(--tn-color-surface-overlay);
  color: var(--tn-color-text);
  border: 1px solid var(--tn-color-border);
  border-left: 4px solid var(--tn-color-border-strong);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-lg);
  animation: tn-toast-in var(--tn-duration-base) var(--tn-easing-standard);
}
.tn-toast--info    { border-left-color: var(--tn-color-accent); }
.tn-toast--success { border-left-color: var(--tn-color-success); }
.tn-toast--warning { border-left-color: var(--tn-color-warning); }
.tn-toast--error   { border-left-color: var(--tn-color-danger); }

.tn-toast__icon { flex: none; display: inline-flex; padding-top: 1px; }
.tn-toast--info .tn-toast__icon    { color: var(--tn-color-accent-soft-ink); }
.tn-toast--success .tn-toast__icon { color: var(--tn-color-success-soft-ink); }
.tn-toast--warning .tn-toast__icon { color: var(--tn-color-warning-soft-ink); }
.tn-toast--error .tn-toast__icon   { color: var(--tn-color-danger-soft-ink); }

.tn-toast__body { flex: 1 1 auto; min-width: 0; }
.tn-toast__title { font-weight: var(--tn-weight-semibold); }
.tn-toast__message { margin-top: 2px; font-size: var(--tn-text-sm); color: var(--tn-color-text-muted); overflow-wrap: anywhere; }
.tn-toast__action { margin-top: var(--tn-space-xs); }

.tn-toast__dismiss {
  flex: none;
  width: var(--tn-size-touch);
  height: var(--tn-size-touch);
  margin: -6px -6px 0 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-color-text-muted);
  font-size: var(--tn-text-lg);
  line-height: 1;
  cursor: pointer;
}
.tn-toast__dismiss:hover { background: var(--tn-color-surface-sunken); color: var(--tn-color-text); }

@keyframes tn-toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

@media (max-width: 639.98px) {
  .tn-toast-viewport {
    left: var(--tn-space-xs);
    right: var(--tn-space-xs);
    width: auto;
    /* Above the bottom navigation, or the toast covers the tab a surveyor is
       trying to reach. */
    bottom: calc(var(--tn-size-bottom-nav-height) + var(--tn-space-xs) + env(safe-area-inset-bottom, 0px));
  }
}


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

@media (forced-colors: active) {
  .tn-btn { border: 1px solid ButtonText; }
  .tn-btn--primary, .tn-btn--danger { forced-color-adjust: none; }
  .tn-badge, .tn-status { border: 1px solid CanvasText; }
  .tn-status__dot { border: 1px solid CanvasText; }
  .tn-table-wrap, .tn-card, .tn-toast, .tn-error { border: 1px solid CanvasText; }
  .tn-nav__link--current { border-bottom-color: Highlight; }
  :focus-visible { outline-color: Highlight; }
  .tn-skeleton { animation: none; border: 1px solid GrayText; }
  /* High contrast throws every colour away, and these two carry their meaning
     in colour alone. A border puts the shape back so a meter still reads as
     full or empty and a swatch still reads as a swatch. */
  .tn-meter { border: 1px solid CanvasText; }
  .tn-meter__fill { background: Highlight; forced-color-adjust: none; }
  .tn-swatch { border: 1px solid CanvasText; }
  .tn-field__control > select { background-image: none; }
}

@media print {
  .tn-shell__header, .tn-nav, .tn-skip, .tn-toast-viewport, .tn-shell__footer,
  .tn-page-header__actions, .tn-error__actions, .tn-empty__action { display: none !important; }
  body { background: #ffffff; color: #000000; font-size: 11pt; }
  .tn-shell__main { max-width: none; padding: 0; }
  .tn-table-wrap, .tn-card { border-color: #000000; box-shadow: none; }
  .tn-btn { box-shadow: none; }
  .tn-table { font-size: 10pt; }
  .tn-table thead { display: table-header-group; }
  .tn-table tr { break-inside: avoid; }
  /* A filed printout has to say where its rows came from. */
  .tn-table__cell-link[href]::after { content: ' (' attr(href) ')'; font-size: 8pt; font-weight: 400; }
}


/* ---------------------------------------------------------------- tones */
.tn-chart__s1 { color: var(--tn-chart-s1, #2a78d6); }
.tn-chart__s2 { color: var(--tn-chart-s2, #eb6834); }
.tn-chart__s3 { color: var(--tn-chart-s3, #1baf7a); }
.tn-chart__s4 { color: var(--tn-chart-s4, #4a3aa7); }
.tn-chart__s5 { color: var(--tn-chart-s5, #e87ba4); }
.tn-chart__s6 { color: var(--tn-chart-s6, #eda100); }
.tn-chart__blue1 { color: var(--tn-chart-blue1, #5598e7); }
.tn-chart__blue2 { color: var(--tn-chart-blue2, #2a78d6); }
.tn-chart__blue3 { color: var(--tn-chart-blue3, #1c5cab); }
.tn-chart__blue4 { color: var(--tn-chart-blue4, #0d366b); }
.tn-chart__orange1 { color: var(--tn-chart-orange1, #df784e); }
.tn-chart__orange2 { color: var(--tn-chart-orange2, #c5521c); }
.tn-chart__orange3 { color: var(--tn-chart-orange3, #9b3b08); }
.tn-chart__orange4 { color: var(--tn-chart-orange4, #702905); }
.tn-chart__unsurveyed { color: var(--tn-chart-unsurveyed, var(--tn-color-status-unsurveyed, #c8362b)); }
.tn-chart__pending { color: var(--tn-chart-pending, var(--tn-color-status-pending, #b87a0d)); }
.tn-chart__approved { color: var(--tn-chart-approved, var(--tn-color-status-approved, #17845a)); }
.tn-chart__rejected { color: var(--tn-chart-rejected, var(--tn-color-status-rejected, #7c3fae)); }
.tn-chart__rent-high { color: var(--tn-chart-rent-high, #d64545); }
.tn-chart__rent-mid { color: var(--tn-chart-rent-mid, #f0a020); }
.tn-chart__rent-low { color: var(--tn-chart-rent-low, #3aa655); }
.tn-chart__rent-vacant { color: var(--tn-chart-rent-vacant, #a9dca3); }
.tn-chart__rent-unrecorded { color: var(--tn-chart-rent-unrecorded, #aab3bb); }
.tn-chart__other { color: var(--tn-chart-other, #7d8791); }
.tn-chart__muted { color: var(--tn-chart-muted, #9aa7a2); }
.tn-chart__navy { color: var(--tn-chart-navy, var(--tn-color-header-bg, #14224a)); }
.tn-chart__accent { color: var(--tn-chart-accent, var(--tn-color-accent, #0f6d64)); }

/* ---------------------------------------------------------------- chart */
.tn-chart {
  position: relative;
  width: 100%;
  min-width: 0;
  color: var(--tn-color-text, #0f1a33);
  font-family: var(--tn-font-sans, system-ui, sans-serif);
  -webkit-tap-highlight-color: transparent;
}
.tn-chart__svg {
  display: block;
  width: 100%;
  overflow: visible;
  font-family: inherit;
}
.tn-chart__grid {
  stroke: var(--tn-color-border, #d8dee9);
  stroke-width: 1;
  shape-rendering: crispEdges;
}
.tn-chart__baseline {
  stroke: var(--tn-color-border-strong, #b4bed0);
  stroke-width: 1;
  shape-rendering: crispEdges;
}
.tn-chart__tick {
  fill: var(--tn-color-text-faint, #566079);
  font-variant-numeric: tabular-nums;
}
.tn-chart__cat { fill: var(--tn-color-text-muted, #434e68); }
.tn-chart__value {
  fill: var(--tn-color-text, #0f1a33);
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.tn-chart__inlabel {
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.tn-chart__inlabel--light { fill: #ffffff; }
.tn-chart__inlabel--dark { fill: #0f1a33; }

.tn-chart__bar { fill: currentColor; transition: opacity var(--tn-duration-fast, 120ms); }
.tn-chart__line {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.tn-chart__area { fill: currentColor; fill-opacity: 0.1; stroke: none; }
.tn-chart__dot {
  fill: currentColor;
  stroke: var(--tn-color-surface-raised, #ffffff);
  stroke-width: 2;
}
.tn-chart__crosshair {
  stroke: var(--tn-color-text-faint, #566079);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

/* A mark: its band is the hit target; the band washes on hover and rings on
   keyboard focus, because a 24px bar is not a target and a ring drawn round
   the bar alone would read as a border. */
.tn-chart__mark { outline: none; cursor: default; }
.tn-chart__hit { fill: transparent; stroke: none; }
.tn-chart__mark:hover .tn-chart__hit,
.tn-chart__mark.is-active .tn-chart__hit { fill: rgba(20, 34, 74, 0.045); }
.tn-chart__column:hover .tn-chart__hit,
.tn-chart__column.is-active .tn-chart__hit { fill: transparent; }
.tn-chart__mark:focus-visible .tn-chart__hit {
  stroke: var(--tn-color-focus-ring, #1e40af);
  stroke-width: 2;
  fill: rgba(30, 64, 175, 0.06);
}
.tn-chart--active .tn-chart__mark:not(.is-active) .tn-chart__bar { opacity: 0.7; }

/* --------------------------------------------- bars, drawn on the server */
/* BarChart lays out in percentages and asks CSS what a width decides: the
   chart and each bar's track are containers, so an inline value appears when
   its segment can hold it, and a vertical chart too narrow for its names shows
   its horizontal twin. No script measures anything. */
.tn-chart--bars { container: tn-chart / inline-size; }
.tn-chart__gap {
  stroke: var(--tn-chart-gap, var(--tn-color-surface-raised, #ffffff));
  stroke-width: 2;
  shape-rendering: crispEdges;
}
.tn-chart__gap-fill { fill: var(--tn-chart-gap, var(--tn-color-surface-raised, #ffffff)); }

.tn-hbar {
  display: grid;
  grid-template-columns: min(var(--tn-hbar-label, 0px), 40%) minmax(0, 1fr) var(--tn-hbar-tip, 16px);
}
.tn-hbar__rows,
.tn-hbar__axis {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  margin: 0;
  padding: 0;
  list-style: none;
}
.tn-hbar__row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  border-radius: 6px;
  outline: none;
  cursor: default;
}
.tn-hbar__cat {
  grid-column: 1;
  min-width: 0;
  padding-right: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  color: var(--tn-color-text-muted, #434e68);
  font-size: 14px;
  line-height: 1.25;
}
.tn-hbar__track {
  grid-column: 2;
  position: relative;
  min-width: 0;
  container: tn-track / inline-size;
}
.tn-hbar__axis > svg { grid-column: 2; }
.tn-hbar__svg,
.tn-hbar__ticks,
.tn-vbar__svg,
.tn-vbar__axis {
  display: block;
  width: 100%;
  overflow: visible;
  font-family: inherit;
}
.tn-chart__inlabel[class*='tn-fit-'] { display: none; }
@container tn-track (min-width: 64px) { .tn-fit-64 { display: inline; } }
@container tn-track (min-width: 80px) { .tn-fit-80 { display: inline; } }
@container tn-track (min-width: 96px) { .tn-fit-96 { display: inline; } }
@container tn-track (min-width: 120px) { .tn-fit-120 { display: inline; } }
@container tn-track (min-width: 144px) { .tn-fit-144 { display: inline; } }
@container tn-track (min-width: 160px) { .tn-fit-160 { display: inline; } }
@container tn-track (min-width: 200px) { .tn-fit-200 { display: inline; } }
@container tn-track (min-width: 240px) { .tn-fit-240 { display: inline; } }
@container tn-track (min-width: 280px) { .tn-fit-280 { display: inline; } }
@container tn-track (min-width: 320px) { .tn-fit-320 { display: inline; } }
@container tn-track (min-width: 360px) { .tn-fit-360 { display: inline; } }
@container tn-track (min-width: 400px) { .tn-fit-400 { display: inline; } }
@container tn-track (min-width: 480px) { .tn-fit-480 { display: inline; } }
@container tn-track (min-width: 560px) { .tn-fit-560 { display: inline; } }
@container tn-track (min-width: 640px) { .tn-fit-640 { display: inline; } }
@container tn-track (min-width: 720px) { .tn-fit-720 { display: inline; } }
@container tn-track (min-width: 800px) { .tn-fit-800 { display: inline; } }
@container tn-track (min-width: 960px) { .tn-fit-960 { display: inline; } }
@container tn-track (min-width: 1120px) { .tn-fit-1120 { display: inline; } }
@container tn-track (min-width: 1280px) { .tn-fit-1280 { display: inline; } }
@container tn-chart (max-width: 400px) { .tn-chart__tick--minor { display: none; } }

.tn-vbar {
  display: grid;
  grid-template-columns: var(--tn-vbar-axis, 4px) repeat(var(--tn-vbar-n, 1), minmax(0, 1fr));
  grid-template-rows: auto auto;
}
.tn-vbar__axis { grid-column: 1; grid-row: 1; }
.tn-vbar__cols {
  grid-column: 2 / -1;
  grid-row: 1 / span 2;
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
  margin: 0;
  padding: 0;
  list-style: none;
}
.tn-vbar__col {
  grid-row: 1 / span 2;
  display: grid;
  grid-template-rows: subgrid;
  min-width: 0;
  border-radius: 6px;
  outline: none;
  cursor: default;
}
.tn-vbar__plot { position: relative; min-width: 0; }
.tn-vbar__cat {
  padding: 6px 2px 0;
  text-align: center;
  color: var(--tn-color-text-muted, #434e68);
  font-size: 14px;
  line-height: 17px;
  overflow-wrap: break-word;
}
.tn-chart--flip > .tn-vbar { display: none; }
@container tn-chart (min-width: 240px) { .tn-chart--flip-240 > .tn-vbar { display: grid; } .tn-chart--flip-240 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 280px) { .tn-chart--flip-280 > .tn-vbar { display: grid; } .tn-chart--flip-280 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 320px) { .tn-chart--flip-320 > .tn-vbar { display: grid; } .tn-chart--flip-320 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 360px) { .tn-chart--flip-360 > .tn-vbar { display: grid; } .tn-chart--flip-360 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 400px) { .tn-chart--flip-400 > .tn-vbar { display: grid; } .tn-chart--flip-400 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 440px) { .tn-chart--flip-440 > .tn-vbar { display: grid; } .tn-chart--flip-440 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 480px) { .tn-chart--flip-480 > .tn-vbar { display: grid; } .tn-chart--flip-480 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 520px) { .tn-chart--flip-520 > .tn-vbar { display: grid; } .tn-chart--flip-520 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 560px) { .tn-chart--flip-560 > .tn-vbar { display: grid; } .tn-chart--flip-560 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 640px) { .tn-chart--flip-640 > .tn-vbar { display: grid; } .tn-chart--flip-640 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 720px) { .tn-chart--flip-720 > .tn-vbar { display: grid; } .tn-chart--flip-720 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 800px) { .tn-chart--flip-800 > .tn-vbar { display: grid; } .tn-chart--flip-800 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 960px) { .tn-chart--flip-960 > .tn-vbar { display: grid; } .tn-chart--flip-960 > .tn-hbar { display: none; } }
@container tn-chart (min-width: 1120px) { .tn-chart--flip-1120 > .tn-vbar { display: grid; } .tn-chart--flip-1120 > .tn-hbar { display: none; } }

/* A category is one target: its band washes on hover, rings on keyboard
   focus, and the other bars step back while it is read. */
.tn-hbar__row:hover,
.tn-vbar__col:hover { background: rgba(20, 34, 74, 0.045); }
.tn-hbar__row:focus-visible,
.tn-vbar__col:focus-visible {
  background: rgba(30, 64, 175, 0.06);
  box-shadow: inset 0 0 0 2px var(--tn-color-focus-ring, #1e40af);
}
.tn-hbar__rows:has(.tn-hbar__row:hover) .tn-hbar__row:not(:hover) .tn-chart__bar,
.tn-vbar__cols:has(.tn-vbar__col:hover) .tn-vbar__col:not(:hover) .tn-chart__bar { opacity: 0.7; }
.tn-chart-tip--hover { display: none; }
.tn-hbar__row:is(:hover, :focus) .tn-chart-tip--hover,
.tn-vbar__col:is(:hover, :focus) .tn-chart-tip--hover { display: block; }

/* --------------------------------------------------------------- tooltip */
.tn-chart-tip {
  position: absolute;
  z-index: 20;
  pointer-events: none;
  min-width: 128px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--tn-color-surface-overlay, #ffffff);
  border: 1px solid var(--tn-color-border, #d8dee9);
  border-radius: var(--tn-radius-md, 10px);
  box-shadow: var(--tn-shadow-lg, 0 2px 4px rgba(15, 26, 51, 0.06), 0 16px 32px -8px rgba(15, 26, 51, 0.16));
  color: var(--tn-color-text, #0f1a33);
  font-size: var(--tn-text-xs);
  line-height: var(--tn-leading-snug);
  white-space: nowrap;
}
.tn-chart-tip--above { transform: translate(-50%, calc(-100% - 10px)); }
.tn-chart-tip--above-start { transform: translate(-20px, calc(-100% - 10px)); }
.tn-chart-tip--above-end { transform: translate(calc(-100% + 20px), calc(-100% - 10px)); }
.tn-chart-tip--right { transform: translate(14px, -50%); }
.tn-chart-tip--left { transform: translate(calc(-100% - 14px), -50%); }
.tn-chart-tip__title {
  margin: 0 0 6px;
  color: var(--tn-color-text-muted, #434e68);
  font-weight: var(--tn-weight-semibold);
}
.tn-chart-tip__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 3px;
}
.tn-chart-tip__row { display: flex; align-items: center; gap: 8px; }
.tn-chart-tip__value {
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--tn-color-text, #0f1a33);
}
.tn-chart-tip__label { color: var(--tn-color-text-muted, #434e68); }
.tn-chart-tip__footer {
  margin: 6px 0 0;
  padding-top: 6px;
  border-top: 1px solid var(--tn-color-border, #d8dee9);
  color: var(--tn-color-text-muted, #434e68);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------ keys, legend */
.tn-chart-key { display: inline-block; flex: none; vertical-align: middle; }
.tn-chart-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  font-size: var(--tn-text-xs);
  color: var(--tn-color-text-muted, #434e68);
}
.tn-chart-legend__item { display: inline-flex; align-items: center; gap: 7px; }
.tn-chart-legend__value {
  color: var(--tn-color-text, #0f1a33);
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ frame */
.tn-chart-frame {
  margin: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-sm, 12px);
}
.tn-chart-frame__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--tn-space-xs, 8px) var(--tn-space-md, 16px);
}
.tn-chart-frame__titles { min-width: 0; flex: 1 1 16rem; }
.tn-chart-frame__title {
  margin: 0;
  color: var(--tn-color-text, #0f1a33);
  font-size: var(--tn-text-base);
  font-weight: var(--tn-weight-semibold);
  line-height: var(--tn-leading-snug);
  letter-spacing: 0;
}
.tn-chart-frame__desc {
  margin: 2px 0 0;
  color: var(--tn-color-text-muted, #434e68);
  font-size: var(--tn-text-sm);
  line-height: var(--tn-leading-snug);
}
.tn-chart-frame__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.tn-chart-frame__toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tn-size-control-compact, 36px);
  padding: 0 12px;
  border: 1px solid var(--tn-color-border-strong, #b4bed0);
  border-radius: var(--tn-radius-pill, 999px);
  background: var(--tn-color-surface-raised, #ffffff);
  color: var(--tn-color-accent, #1e40af);
  font: inherit;
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  cursor: pointer;
  white-space: nowrap;
}
.tn-chart-frame__toggle:hover {
  background: var(--tn-color-accent-soft, #e6ecf9);
  border-color: var(--tn-color-accent, #1e40af);
  color: var(--tn-color-accent-soft-ink, #1b3a8f);
}
.tn-chart-frame__toggle:focus-visible {
  outline: 3px solid var(--tn-color-focus-ring, #1e40af);
  outline-offset: 2px;
  border-radius: var(--tn-radius-pill, 999px);
}
.tn-chart-frame__body { min-width: 0; }
.tn-chart-frame--refreshing .tn-chart-frame__body {
  opacity: 0.5;
  transition: opacity var(--tn-duration-base, 180ms);
}
.tn-chart-frame__foot {
  margin: 0;
  color: var(--tn-color-text-faint, #566079);
  font-size: var(--tn-text-xs);
}
@media (max-width: 639.98px) {
  .tn-chart-frame__toggle { min-height: var(--tn-size-touch, 44px); padding: 0 14px; }
}

/* ------------------------------------------------------------------ table */
.tn-chart-table-wrap { overflow-x: auto; }
.tn-chart-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--tn-text-sm);
  color: var(--tn-color-text, #0f1a33);
}
.tn-chart-table__caption {
  caption-side: top;
  padding: 0 0 8px;
  text-align: left;
  font-weight: var(--tn-weight-semibold);
}
.tn-chart-table th,
.tn-chart-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--tn-color-border, #d8dee9);
  text-align: left;
  vertical-align: top;
  line-height: var(--tn-leading-snug);
}
.tn-chart-table thead th {
  background: var(--tn-color-surface-sunken, #e8ecf4);
  color: var(--tn-color-text-muted, #434e68);
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  white-space: nowrap;
}
.tn-chart-table thead th:first-child { border-top-left-radius: var(--tn-radius-sm, 5px); }
.tn-chart-table thead th:last-child { border-top-right-radius: var(--tn-radius-sm, 5px); }
.tn-chart-table tbody th { font-weight: var(--tn-weight-medium); }
.tn-chart-table .tn-chart-table__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.tn-chart-table tfoot th,
.tn-chart-table tfoot td {
  font-weight: var(--tn-weight-semibold);
  border-bottom: 0;
  border-top: 2px solid var(--tn-color-border-strong, #b4bed0);
}

/* ------------------------------------------------------------------ donut */
.tn-donut {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--tn-space-lg, 24px);
}
.tn-donut__svg { display: block; flex: none; max-width: 100%; height: auto; overflow: visible; }
.tn-donut__track { stroke: var(--tn-color-surface-sunken, #e8ecf4); }
.tn-donut__arc { fill: currentColor; transition: opacity var(--tn-duration-fast, 120ms); }
.tn-donut__seg { outline: none; cursor: default; }
.tn-donut__centre--seg { display: none; }
.tn-donut:has(.tn-donut__seg:is(:hover, :focus), .tn-donut__item:hover) .tn-donut__centre:not(.tn-donut__centre--seg) { display: none; }
.tn-donut:has(.tn-donut__seg[data-seg='0']:is(:hover, :focus), .tn-donut__item[data-seg='0']:hover) .tn-donut__centre--seg[data-seg='0'] { display: inline; }
.tn-donut:has(.tn-donut__seg[data-seg='0']:is(:hover, :focus), .tn-donut__item[data-seg='0']:hover) .tn-donut__seg:not([data-seg='0']) .tn-donut__arc { opacity: 0.4; }
.tn-donut:has(.tn-donut__seg[data-seg='0']:is(:hover, :focus), .tn-donut__item[data-seg='0']:hover) .tn-donut__item[data-seg='0'] { background: var(--tn-color-surface, #f4f6fa); }
.tn-donut:has(.tn-donut__seg[data-seg='1']:is(:hover, :focus), .tn-donut__item[data-seg='1']:hover) .tn-donut__centre--seg[data-seg='1'] { display: inline; }
.tn-donut:has(.tn-donut__seg[data-seg='1']:is(:hover, :focus), .tn-donut__item[data-seg='1']:hover) .tn-donut__seg:not([data-seg='1']) .tn-donut__arc { opacity: 0.4; }
.tn-donut:has(.tn-donut__seg[data-seg='1']:is(:hover, :focus), .tn-donut__item[data-seg='1']:hover) .tn-donut__item[data-seg='1'] { background: var(--tn-color-surface, #f4f6fa); }
.tn-donut:has(.tn-donut__seg[data-seg='2']:is(:hover, :focus), .tn-donut__item[data-seg='2']:hover) .tn-donut__centre--seg[data-seg='2'] { display: inline; }
.tn-donut:has(.tn-donut__seg[data-seg='2']:is(:hover, :focus), .tn-donut__item[data-seg='2']:hover) .tn-donut__seg:not([data-seg='2']) .tn-donut__arc { opacity: 0.4; }
.tn-donut:has(.tn-donut__seg[data-seg='2']:is(:hover, :focus), .tn-donut__item[data-seg='2']:hover) .tn-donut__item[data-seg='2'] { background: var(--tn-color-surface, #f4f6fa); }
.tn-donut:has(.tn-donut__seg[data-seg='3']:is(:hover, :focus), .tn-donut__item[data-seg='3']:hover) .tn-donut__centre--seg[data-seg='3'] { display: inline; }
.tn-donut:has(.tn-donut__seg[data-seg='3']:is(:hover, :focus), .tn-donut__item[data-seg='3']:hover) .tn-donut__seg:not([data-seg='3']) .tn-donut__arc { opacity: 0.4; }
.tn-donut:has(.tn-donut__seg[data-seg='3']:is(:hover, :focus), .tn-donut__item[data-seg='3']:hover) .tn-donut__item[data-seg='3'] { background: var(--tn-color-surface, #f4f6fa); }
.tn-donut:has(.tn-donut__seg[data-seg='4']:is(:hover, :focus), .tn-donut__item[data-seg='4']:hover) .tn-donut__centre--seg[data-seg='4'] { display: inline; }
.tn-donut:has(.tn-donut__seg[data-seg='4']:is(:hover, :focus), .tn-donut__item[data-seg='4']:hover) .tn-donut__seg:not([data-seg='4']) .tn-donut__arc { opacity: 0.4; }
.tn-donut:has(.tn-donut__seg[data-seg='4']:is(:hover, :focus), .tn-donut__item[data-seg='4']:hover) .tn-donut__item[data-seg='4'] { background: var(--tn-color-surface, #f4f6fa); }
.tn-donut:has(.tn-donut__seg[data-seg='5']:is(:hover, :focus), .tn-donut__item[data-seg='5']:hover) .tn-donut__centre--seg[data-seg='5'] { display: inline; }
.tn-donut:has(.tn-donut__seg[data-seg='5']:is(:hover, :focus), .tn-donut__item[data-seg='5']:hover) .tn-donut__seg:not([data-seg='5']) .tn-donut__arc { opacity: 0.4; }
.tn-donut:has(.tn-donut__seg[data-seg='5']:is(:hover, :focus), .tn-donut__item[data-seg='5']:hover) .tn-donut__item[data-seg='5'] { background: var(--tn-color-surface, #f4f6fa); }
.tn-donut__seg:focus-visible .tn-donut__arc {
  stroke: var(--tn-color-focus-ring, #1e40af);
  stroke-width: 3;
  paint-order: stroke;
}
.tn-donut__value {
  fill: var(--tn-color-text, #0f1a33);
  font-weight: var(--tn-weight-semibold);
  letter-spacing: var(--tn-tracking-tight);
}
.tn-donut__label { fill: var(--tn-color-text-muted, #434e68); }
.tn-donut__legend {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 200px;
  min-width: 0;
  display: grid;
  gap: 2px;
}
.tn-donut__item {
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr) auto 4.25em;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border-radius: var(--tn-radius-sm, 5px);
  font-size: var(--tn-text-sm);
}
.tn-donut__name { color: var(--tn-color-text, #0f1a33); overflow-wrap: anywhere; }
.tn-donut__num {
  color: var(--tn-color-text, #0f1a33);
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.tn-donut__share {
  color: var(--tn-color-text-faint, #566079);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* -------------------------------------------------------------- sparkline */
.tn-sparkline { display: inline-block; vertical-align: middle; overflow: visible; }
.tn-sparkline__line { stroke-width: 1.75; }

/* ----------------------------------------------------------------- states */
.tn-chart-skeleton {
  display: block;
  color: var(--tn-color-skeleton-base, #e1e6ef);
  animation: tn-chart-pulse 1.6s ease-in-out infinite;
}
@keyframes tn-chart-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}
.tn-chart-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 160px;
  padding: var(--tn-space-lg, 24px);
  border-radius: var(--tn-radius-md, 10px);
  background: var(--tn-color-surface, #f4f6fa);
  text-align: center;
}
.tn-chart-state__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-bottom: 4px;
  border-radius: 50%;
  background: var(--tn-color-surface-raised, #ffffff);
  color: var(--tn-color-text-faint, #566079);
}
.tn-chart-state--error .tn-chart-state__icon {
  background: var(--tn-color-danger-soft, #fbe8e5);
  color: var(--tn-color-danger, #9c2f26);
}
.tn-chart-state__title {
  margin: 0;
  color: var(--tn-color-text, #0f1a33);
  font-weight: var(--tn-weight-semibold);
}
.tn-chart-state__message {
  margin: 0 0 6px;
  max-width: 44ch;
  color: var(--tn-color-text-muted, #434e68);
  font-size: var(--tn-text-sm);
}

/* ----------------------------------------------------- motion, contrast */
@media (prefers-reduced-motion: reduce) {
  .tn-chart-skeleton { animation: none; }
  .tn-chart__bar, .tn-donut__arc, .tn-chart-frame--refreshing .tn-chart-frame__body { transition: none; }
}

/* Windows high contrast: the data keeps its colours (they ARE the data),
   the words and the furniture take the system's. */
@media (forced-colors: active) {
  .tn-chart__svg, .tn-donut__svg, .tn-sparkline, .tn-chart-key,
  .tn-hbar__svg, .tn-hbar__ticks, .tn-vbar__svg, .tn-vbar__axis { forced-color-adjust: none; }
  .tn-chart__gap { stroke: Canvas; }
  .tn-chart__gap-fill { fill: Canvas; }
  .tn-chart__tick, .tn-chart__cat, .tn-chart__value, .tn-donut__value, .tn-donut__label { fill: CanvasText; }
  .tn-chart__grid, .tn-chart__baseline, .tn-chart__crosshair { stroke: GrayText; }
  .tn-chart__dot { stroke: Canvas; }
  .tn-chart__mark:focus-visible .tn-chart__hit { stroke: Highlight; }
  .tn-hbar__row:focus-visible, .tn-vbar__col:focus-visible { box-shadow: inset 0 0 0 2px Highlight; }
  .tn-chart-tip { border-color: CanvasText; }
  .tn-chart-skeleton { color: GrayText; }
}

/* On paper both twins print: the drawing and its table. */
@media print {
  .tn-chart-frame__chart[hidden], .tn-chart-frame__table[hidden] { display: block; }
  .tn-chart-frame__table { margin-top: 12px; }
  .tn-chart-frame__toggle, .tn-chart-tip { display: none; }
  .tn-chart-frame--refreshing .tn-chart-frame__body { opacity: 1; }
}


.tn-illo {
  display: block;
  max-width: 100%;
  height: auto;
  flex-shrink: 0;
  stroke: var(--tn-illo-ink, var(--tn-color-header-bg, #14224a));
}
.tn-illo__tint { fill: var(--tn-illo-tint, #dcecf3); }
.tn-illo__tint-line { stroke: var(--tn-illo-tint, #dcecf3); }
.tn-illo__paper { fill: var(--tn-illo-paper, #ffffff); }
.tn-illo__paper-line { stroke: var(--tn-illo-paper, #ffffff); }
.tn-illo__accent { fill: var(--tn-illo-accent, #eb6834); }
.tn-illo__accent-line { stroke: var(--tn-illo-accent, #eb6834); }
.tn-illo__ink { fill: var(--tn-illo-ink, var(--tn-color-header-bg, #14224a)); }

/* EmptyState's illustration slot. The drawing is scenery, so it is kept well
   below the width of the sentence under it: 240px on a page, 160px in a card. */
.tn-empty__art {
  display: block;
  width: min(100%, 240px);
  margin: 0 auto var(--tn-space-xs, 8px);
}
.tn-empty--sm .tn-empty__art { width: min(100%, 160px); }
.tn-empty__art > .tn-illo { width: 100%; }

@media (forced-colors: active) {
  .tn-illo { stroke: CanvasText; }
  .tn-illo__tint, .tn-illo__paper { fill: Canvas; }
  .tn-illo__accent, .tn-illo__ink { fill: CanvasText; }
  .tn-illo__accent-line, .tn-illo__tint-line { stroke: CanvasText; }
  .tn-illo__paper-line { stroke: Canvas; }
}

@media print {
  .tn-empty__art { display: none; }
}


/* ------------------------------------------------------------ scale bar */

.tn-map-scale {
  display: inline-flex;
  align-items: flex-end;
  line-height: 0;
  pointer-events: none;
  user-select: none;
}
.tn-map-scale--plate {
  padding: 2px 6px 4px;
  border-radius: var(--tn-radius-sm, 6px);
  background: rgba(255, 255, 255, 0.88);
  box-shadow: 0 1px 3px rgba(11, 23, 51, 0.28);
}
.tn-map-scale svg { display: block; overflow: visible; }
.tn-map-scale__text {
  fill: var(--tn-map-ink, var(--tn-color-header-bg, #14224a));
  font-family: var(--tn-font-sans, system-ui, sans-serif);
  font-size: var(--tn-text-xs);
  font-weight: var(--tn-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.tn-map-scale__ink { fill: var(--tn-map-ink, var(--tn-color-header-bg, #14224a)); }
.tn-map-scale__paper { fill: #ffffff; }
.tn-map-scale__edge,
.tn-map-scale__line { stroke: var(--tn-map-ink, var(--tn-color-header-bg, #14224a)); }

/* ---------------------------------------------------------- north arrow */

.tn-map-north { display: block; flex-shrink: 0; }
.tn-map-north--badge { filter: drop-shadow(0 1px 2px rgba(11, 23, 51, 0.3)); }
.tn-map-north__ink { fill: var(--tn-map-ink, var(--tn-color-header-bg, #14224a)); stroke: var(--tn-map-ink, var(--tn-color-header-bg, #14224a)); }
.tn-map-north__paper { stroke: var(--tn-map-ink, var(--tn-color-header-bg, #14224a)); }
.tn-map-north__letter { stroke: var(--tn-map-ink, var(--tn-color-header-bg, #14224a)); }

/* ------------------------------------------------------- selection pin */

.tn-map-pin { overflow: visible; }
svg.tn-map-pin { display: block; }
.tn-map-pin__body { fill: var(--tn-map-pin, #e02d2d); }

/* -------------------------------------------------------- legend symbol */

.tn-map-key { display: block; flex-shrink: 0; overflow: visible; }

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

.tn-map-ctl {
  display: inline-grid;
  place-items: center;
  /* The control token: 40px under a mouse, 44px under a finger or below 640px. */
  width: var(--tn-size-control, 40px);
  height: var(--tn-size-control, 40px);
  padding: 0;
  border: 0;
  border-radius: var(--tn-radius-md, 8px);
  background: #ffffff;
  color: var(--tn-map-ink, var(--tn-color-header-bg, #14224a));
  box-shadow: 0 1px 4px rgba(11, 23, 51, 0.3);
  cursor: pointer;
  transition: background-color var(--tn-duration-fast, 120ms) var(--tn-easing-standard, ease),
              color var(--tn-duration-fast, 120ms) var(--tn-easing-standard, ease);
}
.tn-map-ctl:hover { background: var(--tn-color-surface, #f4f6fa); }
.tn-map-ctl:active { background: var(--tn-color-surface-sunken, #e8ecf4); }
.tn-map-ctl:focus-visible {
  outline: 3px solid var(--tn-color-focus-ring, #1e40af);
  outline-offset: 2px;
}
.tn-map-ctl[aria-pressed='true'] {
  background: var(--tn-map-ink, var(--tn-color-header-bg, #14224a));
  color: #ffffff;
}
.tn-map-ctl:disabled { cursor: not-allowed; color: var(--tn-color-text-faint, #566079); box-shadow: 0 1px 2px rgba(11, 23, 51, 0.2); }
.tn-map-ctl > svg { display: block; }

.tn-map-ctl-group {
  display: inline-flex;
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--tn-radius-md, 8px);
  background: #ffffff;
  box-shadow: 0 1px 4px rgba(11, 23, 51, 0.3);
}
.tn-map-ctl-group--vertical { flex-direction: column; }
.tn-map-ctl-group > .tn-map-ctl { box-shadow: none; border-radius: 0; }
.tn-map-ctl-group--vertical > .tn-map-ctl + .tn-map-ctl { border-top: 1px solid var(--tn-color-border, #d8dee9); }
.tn-map-ctl-group--horizontal > .tn-map-ctl + .tn-map-ctl { border-left: 1px solid var(--tn-color-border, #d8dee9); }
.tn-map-ctl-group--vertical > .tn-map-ctl:first-child { border-radius: var(--tn-radius-md, 8px) var(--tn-radius-md, 8px) 0 0; }
.tn-map-ctl-group--vertical > .tn-map-ctl:last-child { border-radius: 0 0 var(--tn-radius-md, 8px) var(--tn-radius-md, 8px); }
.tn-map-ctl-group--horizontal > .tn-map-ctl:first-child { border-radius: var(--tn-radius-md, 8px) 0 0 var(--tn-radius-md, 8px); }
.tn-map-ctl-group--horizontal > .tn-map-ctl:last-child { border-radius: 0 var(--tn-radius-md, 8px) var(--tn-radius-md, 8px) 0; }

@media (pointer: coarse) {
  .tn-map-ctl { width: 44px; height: 44px; }
}

/* ---------------------------------------------------------- legend card */

.tn-map-legend {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 180px;
  padding: 12px 14px;
  border-radius: var(--tn-radius-md, 8px);
  font-family: var(--tn-font-sans, system-ui, sans-serif);
  font-size: var(--tn-text-sm);
  box-shadow: 0 2px 10px rgba(11, 23, 51, 0.28);
}
.tn-map-legend--navy {
  background: rgba(20, 34, 74, 0.94);
  color: #ffffff;
}
.tn-map-legend--light {
  background: #ffffff;
  color: var(--tn-color-text, #0f1a33);
  border: 1px solid var(--tn-color-border, #d8dee9);
}
.tn-map-legend__title { font-weight: var(--tn-weight-semibold); font-size: var(--tn-text-base); }
.tn-map-legend__list { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.tn-map-legend__row { display: flex; align-items: center; gap: 10px; min-height: 22px; }
.tn-map-legend__symbol { display: inline-grid; place-items: center; flex-shrink: 0; width: 24px; }
.tn-map-legend__label { flex: 1 1 auto; min-width: 0; }
.tn-map-legend__count { font-variant-numeric: tabular-nums; opacity: 0.85; }
.tn-map-legend--light .tn-map-legend__count { color: var(--tn-color-text-muted, #434e68); opacity: 1; }

/* ------------------------------------------------------ forced colours */

@media (forced-colors: active) {
  .tn-map-scale--plate { background: Canvas; box-shadow: none; border: 1px solid CanvasText; }
  .tn-map-scale__text, .tn-map-scale__ink { fill: CanvasText; }
  .tn-map-scale__paper { fill: Canvas; }
  .tn-map-scale__edge, .tn-map-scale__line { stroke: CanvasText; }
  .tn-map-north__ink { fill: CanvasText; stroke: CanvasText; }
  .tn-map-north__paper { fill: Canvas; stroke: CanvasText; }
  .tn-map-north__letter { stroke: CanvasText; }
  .tn-map-north__disc { fill: Canvas; stroke: CanvasText; }
  .tn-map-pin__body { fill: Highlight; stroke: Canvas; }
  .tn-map-ctl { border: 1px solid ButtonText; }
  .tn-map-ctl[aria-pressed='true'] { background: Highlight; color: HighlightText; }
  .tn-map-legend { border: 1px solid CanvasText; }
}

@media (prefers-reduced-motion: reduce) {
  .tn-map-ctl { transition: none; }
}

@media print {
  .tn-map-ctl, .tn-map-ctl-group { display: none; }
  .tn-map-north--badge { filter: none; }
}


.qtr-standalone { display: flex; flex-direction: column; gap: var(--tn-space-lg); max-width: var(--tn-size-content-max); margin: 0 auto; padding: var(--tn-space-xl) var(--tn-space-md); }
.qtr-quiet { color: var(--tn-color-text-faint); }
.qtr-noaccess { max-width: var(--tn-size-prose-max); }
.qtr-noaccess__list { margin: 0; padding-left: var(--tn-space-lg); }
.qtr-noaccess__list li { margin-bottom: var(--tn-space-xxs); }
