/* AP-Signoff base stylesheet.
   Built on the ENTIRETEC Corporate Identity (see docs/THEME.md):
   ENTIRETEC red is used sparingly (H1, primary actions, critical emphasis);
   gray and blue carry the rest. Mobile-first; light + dark theme via the
   semantic tokens below (toggled by static/js/theme.js). */

/* ---------- Fonts (self-hosted, no external origins) ---------- */
@font-face {
  font-family: "ENTIRETEC";
  src: url("../fonts/ENTIRETEC-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "ENTIRETEC";
  src: url("../fonts/ENTIRETEC-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "ENTIRETEC";
  src: url("../fonts/ENTIRETEC-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ---------- Design tokens ---------- */
:root {
  --et-red: #920000;
  --et-gray-10: #eceeee;
  --et-gray-20: #d8dadc;
  --et-gray-30: #c2c5c7;
  --et-gray-40: #adaeb2;
  --et-gray-50: #97979b;
  --et-gray-60: #807f84;
  --et-gray-80: #505050;
  --et-blue-light: #b5e2ef;
  --et-blue-medium: #82c0d1;
  --et-blue-dark: #3e9bb0;
  --et-blue-dark-hover: #2c6f80; /* darker teal for link hover (was jarring red) */

  --font-stack: "ENTIRETEC", Arial, Helvetica, sans-serif;
  --page-bg: #ffffff;
  --surface-bg: var(--et-gray-10);
  --control-bg: #ffffff; /* inputs, secondary/outline buttons */
  --text: var(--et-gray-80);
  --text-muted: var(--et-gray-60);
  --heading: #262626; /* near-black headings; red is kept as a small accent */
  --border: var(--et-gray-30);
  --divider: var(--et-gray-20); /* table rows, card outlines, panel borders */
  --link: var(--et-blue-dark);
  --link-hover: var(--et-blue-dark-hover);
  --accent: var(--et-red); /* small red accents: H1 bar, active mode underline */
  --danger-text: var(--et-red); /* error text + outline-danger; readable per theme */
  --primary-hover: #7a0000;
  --info-bg: var(--et-blue-light);
  --info-text: var(--et-gray-80);
  --warn-bg: #fbe3e3; /* red-tinted surface for the "already registered" notice */
  --warn-text: #7a0000;
  color-scheme: light;

  --radius: 6px;
  --tap-target: 44px;
  --content-max: 60rem;
  --space: 1rem;
}

/* Dark theme: applied by static/js/theme.js (persisted per-device choice,
   defaults to the system preference). The CI palette defines no dark colors,
   so these are derived: deep-gray surfaces, CI gray 40% for muted text (the
   CI page marks it "on dark backgrounds"), lightened blue/red where the light
   values would fail contrast on dark. Header/footer keep the CI deep-gray
   look in both themes; red stays a sparing accent. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --page-bg: #1c1e21;
  --surface-bg: #26292d;
  --control-bg: #2c2f34;
  --text: #d6d8db;
  --text-muted: var(--et-gray-40);
  --heading: #f1f2f3;
  --border: #4b4f55;
  --divider: #393d42;
  --link: #6cc4d9;
  --link-hover: #9adcea;
  --accent: #c94f4f;
  --danger-text: #ff9494;
  --primary-hover: #ab1a1a; /* hover brightens; darkening would vanish on dark */
  --info-bg: #23404b;
  --info-text: #d8eef5;
  --warn-bg: #40282a;
  --warn-text: #ffc9c9;
}

/* ---------- Reset-ish base ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* The `hidden` attribute must always win over component display rules
   (e.g. the scanner overlay's `display: flex`), or hidden elements show. */
[hidden] {
  display: none !important;
}

html {
  font-size: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-stack);
  font-weight: 400;
  line-height: 1.5;
  color: var(--text);
  background: var(--page-bg);
}

img {
  max-width: 100%;
  height: auto;
}

a {
  color: var(--link);
  text-decoration: underline;
}
a:hover {
  color: var(--link-hover);
}

:focus-visible {
  outline: 3px solid var(--et-blue-dark);
  outline-offset: 2px;
}

/* ---------- Skip link ---------- */
.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--control-bg);
  color: var(--text);
  padding: 0.5rem 1rem;
  z-index: 100;
}
.skip-link:focus {
  left: 0;
}

/* ---------- Header (deep-gray background, white text) ---------- */
.site-header {
  background: var(--et-gray-80);
  color: #fff;
}
.site-header__inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0.75rem var(--space);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  /* Stay on ONE row at every width so DE/EN never drops below the logo. */
  flex-wrap: nowrap;
}
.site-header__brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: #fff;
  text-decoration: none;
  min-width: 0; /* allow the app name to shrink/ellipsize before wrapping */
  flex: 0 1 auto;
}
.site-header__brand:hover {
  color: #fff; /* brand text stays white; never the global link-hover colour */
}
.site-header__logo {
  /* The ENTIRETEC wordmark is dark + grey on transparency; a white chip keeps
     it legible on the dark header (the grey "TEC" otherwise disappears).
     Same height as the DE/EN chips: they read as the same kind of element, so
     differing heights looked like a mistake. */
  height: var(--tap-target);
  box-sizing: border-box;
  padding: 7px 9px;
  width: auto;
  display: block;
  flex: 0 0 auto;
  background: #fff;
  border-radius: var(--radius);
}
/* The compact logo mark below 34rem leaves room for the name on every current
   phone, so it fits rather than truncating to "AP-..." as it used to between
   26rem and 34rem (the full wordmark plus the name needs ~500px). Below ~22rem
   it does ellipsise - narrower than any phone in use, and an ellipsis beats a
   horizontally scrolling header. */
.site-header__app {
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Language + theme toggles (right side of the header bar) */
.site-header__tools {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  flex: 0 0 auto;
}
.lang-nav {
  display: inline-flex;
  gap: 0.25rem;
  flex: 0 0 auto;
}
.lang-nav__item {
  min-width: var(--tap-target);
  min-height: var(--tap-target);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 0.5rem;
  border-radius: var(--radius);
  color: #fff;
  text-decoration: none;
  font-weight: 600;
}
.lang-nav__item:hover {
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
}
.lang-nav__item--active {
  background: #fff;
  color: var(--et-gray-80);
}

/* Theme toggle: same chip look as the language items. Hidden until theme.js
   stamps data-theme on <html> - without JS the toggle could not work anyway. */
.theme-toggle {
  display: none;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  color: #fff;
}
/* Inline SVG rather than an emoji: 🌙/☀️ fall back to a tofu box wherever no
   emoji font is installed. */
.theme-toggle svg {
  width: 1.15rem;
  height: 1.15rem;
  display: block;
  fill: currentColor;
}
:root[data-theme] .theme-toggle {
  display: inline-flex;
}
:root[data-theme="dark"] .theme-toggle__when-light {
  display: none;
}
:root:not([data-theme="dark"]) .theme-toggle__when-dark {
  display: none;
}

/* ---------- Main content ---------- */
.site-main {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: calc(var(--space) * 1.5) var(--space);
}

/* First-level headline: uppercase (CI rule) in near-black, with a small
   ENTIRETEC-red accent bar instead of a heavy all-red heading. */
.page-title {
  color: var(--heading);
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: 0.03em;
  margin: 0 0 0.85rem;
  font-size: 1.75rem;
}
.page-title::after {
  content: "";
  display: block;
  width: 2.5rem;
  height: 3px;
  margin-top: 0.5rem;
  background: var(--accent);
  border-radius: 2px;
}
.lead {
  color: var(--text);
  margin: 0 0 1.5rem;
  font-size: 1.05rem;
}

h2 {
  font-weight: 600;
  color: var(--text);
}

/* ---------- Cards ---------- */
.cards {
  display: grid;
  gap: var(--space);
  grid-template-columns: 1fr;
}
.card {
  background: var(--surface-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: calc(var(--space) * 1.25);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.card__title {
  margin: 0;
  font-size: 1.2rem;
}
.card__desc {
  margin: 0 0 0.5rem;
  flex: 1;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-target);
  padding: 0.6rem 1.25rem;
  border-radius: var(--radius);
  border: 1px solid transparent;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  text-align: center;
}
/* Primary action: ENTIRETEC red, used sparingly. */
.btn--primary {
  background: var(--et-red);
  color: #fff;
  border-color: var(--et-red);
}
.btn--primary:hover {
  background: var(--primary-hover);
  color: #fff;
}
/* Secondary action: neutral, keeps red exclusive to the primary path. */
.btn--secondary {
  background: var(--control-bg);
  color: var(--text);
  border-color: var(--border);
}
.btn--secondary:hover {
  background: var(--surface-bg);
  color: var(--text);
  border-color: var(--et-blue-dark);
}

/* ---------- Flash messages ---------- */
/* Floating, not in the flow: as a block above <main> every confirmation pushed
   the whole page down and the next click snapped it back. Fixed to the top,
   above the sticky filter bar, dismissed on click and auto-faded by admin.js
   (without JS they simply stay - visible, and still not shifting anything). */
.flashes {
  position: fixed;
  top: 0.75rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  width: min(34rem, calc(100vw - 1.5rem));
  display: grid;
  gap: 0.5rem;
}
.flash {
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  background: var(--surface-bg);
}
/* Only the floating ones: .flash is reused inline in forms, which must not
   become clickable or shadowed. */
.flashes .flash {
  cursor: pointer;
  box-shadow: 0 6px 20px rgb(0 0 0 / 25%);
  opacity: 1;
  transition: opacity 0.4s ease;
}
.flashes .flash[data-fading] {
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .flashes .flash {
    transition: none;
  }
}
.flash--info {
  border-left-color: var(--et-blue-dark);
  background: var(--info-bg);
  color: var(--info-text);
}
.flash--success {
  border-left-color: var(--et-blue-medium);
}
.flash--error {
  border-left-color: var(--et-red);
}

/* ---------- Footer (deep-gray background, white text) ---------- */
.site-footer {
  background: var(--et-gray-80);
  color: #fff;
  margin-top: calc(var(--space) * 2);
}
/* Centred on phones: aligning with the header/field edge reads as an
   off-centre stray line on a narrow screen. Left-aligned from 40rem up,
   where it lines up with the content column again. */
.site-footer__inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--space);
  font-size: 0.9rem;
  text-align: center;
}

/* ---------- Forms ---------- */
.narrow {
  max-width: 32rem;
}
.form {
  display: grid;
  gap: 1.1rem;
  max-width: 40rem;
}
.field {
  display: grid;
  gap: 0.35rem;
}
.field > label {
  font-weight: 600;
}
/* "(optional)" as a blue pill rather than quiet grey text - as grey it read as
   part of the label and installers kept missing which fields they had to fill.
   Anything without the pill is required, so there is no second marker and no
   legend to read. */
.field__opt {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  background: var(--info-bg);
  color: var(--info-text);
  font-weight: 600;
  font-size: 0.75rem;
  white-space: nowrap;
  vertical-align: middle;
}
.field__hint {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}
.input {
  width: 100%;
  min-height: var(--tap-target);
  padding: 0.55rem 0.7rem;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--control-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
textarea.input {
  min-height: 3rem;
  resize: vertical;
}
.input:focus-visible {
  border-color: var(--et-blue-dark);
  outline: 3px solid var(--et-blue-dark);
  outline-offset: 1px;
}
.field--error .input {
  border-color: var(--danger-text);
}
.field__error {
  margin: 0;
  color: var(--danger-text);
  font-size: 0.9rem;
  font-weight: 600;
}
/* "AP already registered" - red, because re-submitting supersedes the existing
   revision. Still not a blocking error: the installer may proceed. */
.field__warning {
  margin: 0.25rem 0 0;
  padding: 0.5rem 0.7rem;
  border-left: 4px solid var(--danger-text);
  background: var(--warn-bg);
  color: var(--warn-text);
  border-radius: var(--radius);
  font-size: 0.95rem;
  font-weight: 600;
}
/* Neutral counterpart: a saved arrival, a known AP in report mode - facts, not
   warnings. */
.field__note {
  margin: 0.25rem 0 0;
  padding: 0.5rem 0.7rem;
  border-left: 4px solid var(--et-blue-dark);
  background: var(--info-bg);
  color: var(--info-text);
  border-radius: var(--radius);
  font-size: 0.95rem;
}
/* Arrival tab body: the button plus its hint and confirmation. The button is
   the tab's whole point, so it gets room rather than sitting inline. */
.arrival {
  padding: 0.75rem;
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  background: var(--surface-bg);
}
.arrival > .btn {
  justify-self: start;
  min-width: 14rem;
}
/* "No arrival recorded today" - a red note on the AP/Report tabs with a
   label that switches to the Arrival tab. */
.arrival-missing {
  display: grid;
  gap: 0.3rem;
}
.arrival-missing__switch {
  width: max-content;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}
/* Mode-independent hiding (the [data-mode] rules own display:, so a second
   display: toggle there would fight them). */
.is-hidden {
  display: none !important;
}
.remember {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 400;
  font-size: 0.9rem;
  color: var(--text);
}
.remember input {
  width: 1.1rem;
  height: 1.1rem;
}
.inv-row {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
}
.inv-row .input {
  flex: 1;
}
.inv-row .btn {
  white-space: nowrap;
}
.form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}
.btn-link {
  background: none;
  border: none;
  padding: 0.5rem 0;
  color: var(--link);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.btn-link:hover {
  color: var(--link-hover);
}

/* ---------- Photo previews ---------- */
.previews {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.5rem;
}
.preview {
  position: relative;
  width: 96px;
}
.preview__img {
  width: 96px;
  height: 96px;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  display: block;
}
.preview__remove {
  margin-top: 0.25rem;
  width: 100%;
  min-height: 32px;
  border: 1px solid var(--border);
  background: var(--control-bg);
  color: var(--text);
  border-radius: var(--radius);
  font: inherit;
  font-size: 0.8rem;
  cursor: pointer;
}

/* ---------- Barcode scanner overlay ---------- */
/* The overlay must fit a phone screen. A portrait camera stream is ~1.8x as
   tall as it is wide, so at full width the preview alone ate more than the
   viewport and the controls were pushed off the bottom edge - unreachable,
   because nothing here scrolled. Two guards, no viewport units (so it holds
   under iOS Larger Text / Display Zoom and beside the Safari toolbars):
   the preview gives up its space to the controls, and if even that is not
   enough the overlay itself scrolls. */
.scanner {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  justify-content: center;
  padding: 1rem;
  overflow-y: auto;
  overscroll-behavior: contain; /* don't drag the page behind it */
}
.scanner__box {
  width: 100%;
  max-width: 30rem;
  max-height: 100%;
  /* Centres like `align-items: center` but, unlike it, overflow stays
     reachable instead of being clipped off the top. */
  margin: auto;
  display: grid;
  /* Only the first row is declared: the preview takes whatever the controls
     leave, down to 6rem, and everything after it sits in an implicit auto row. */
  grid-template-rows: minmax(6rem, 1fr);
  gap: 0.75rem;
  justify-items: center;
}
/* The live preview and the captured still share row 1 / column 1, so showing
   the still costs no height and cannot reintroduce the overflow above. The
   still comes later in the DOM, so it paints on top. */
.scanner__video,
.scanner__still {
  grid-row: 1;
  grid-column: 1;
  width: 100%;
  min-height: 0; /* without this the intrinsic height wins and 1fr cannot shrink */
  max-height: 100%;
  /* Fill the capped strip by cropping, not by letterboxing a portrait stream
     into a narrow sliver. Safe for scanning: the decoder reads the full stream
     (and crops centrally itself), so whatever is visible is certainly
     captured. */
  object-fit: cover;
  border-radius: var(--radius);
  background: #000;
}
.scanner__still {
  display: grid; /* so the image can fill the button */
  padding: 0;
  border: 0;
  overflow: hidden;
  cursor: pointer;
}
/* Whichever of the canvas / img is holding the frozen frame. */
.scanner__still > * {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: cover;
}
.scanner__msg {
  color: #fff;
  margin: 0;
  text-align: center;
}
.scanner__diag {
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.75rem;
  margin: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}
/* Camera-app control bar: shutter centred, torch and close flanking it. The
   columns are assigned explicitly, so hiding the torch (unsupported) or the
   shutter (no live stream) leaves the rest exactly where it was. */
.scanner__bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  width: 100%;
}
#scan-torch {
  grid-column: 1;
  justify-self: center;
}
#scan-shoot {
  grid-column: 2;
}
#scan-close {
  grid-column: 3;
  justify-self: center;
}
/* Icon-only: the glyph is the label, so these carry aria-label + title. */
.scanner__icon {
  display: grid;
  place-items: center;
  width: var(--tap-target);
  height: var(--tap-target);
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: #fff;
  cursor: pointer;
}
.scanner__icon svg {
  width: 1.5rem;
  height: 1.5rem;
  fill: currentColor;
}
.scanner__icon:hover {
  background: rgba(255, 255, 255, 0.14);
}
/* Torch on: filled, so its state is visible and not only announced. */
.scanner__icon[aria-pressed="true"] {
  background: #fff;
  color: #111;
}
/* The shutter: the one obvious main action, shaped like a camera's. */
.scanner__shutter {
  width: 4rem;
  height: 4rem;
  padding: 0;
  border: 3px solid #fff;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.25);
  box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.55);
  cursor: pointer;
}
.scanner__shutter:hover {
  background: rgba(255, 255, 255, 0.45);
}
.scanner__shutter:active {
  transform: scale(0.94);
}
.scanner__icon:focus-visible,
.scanner__shutter:focus-visible {
  outline: 3px solid var(--et-blue-dark);
  outline-offset: 2px;
}
/* The two alternative sources: equal halves, so they read as one pair rather
   than as two more buttons of arbitrary width. */
.scanner__sources {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  width: 100%;
}
.scanner__sources .btn {
  width: 100%;
}
.scanner__icon:disabled,
.scanner__shutter:disabled,
.scanner__sources .btn:disabled {
  opacity: 0.45;
  cursor: default;
}
/* Says what each button will actually do - "Take photo" opening a camera app
   was read as the scan having hung. Hidden while a still is being read, where
   the status message is the thing to look at. */
.scanner__help {
  color: rgba(255, 255, 255, 0.75);
  font-size: 0.8rem;
  line-height: 1.35;
  margin: 0;
  text-align: center;
  max-width: 28rem;
}

/* ---------- Admin ---------- */
/* Tab bar: the active tab is a box that opens into the page; inactive tabs are
   muted labels. Marked via aria-current so the state is also announced. */
.admin-bar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
  border-bottom: 1px solid var(--divider);
  flex-wrap: wrap;
}
.admin-nav {
  display: flex;
  gap: 0.25rem;
}
.admin-nav a {
  min-height: var(--tap-target);
  display: inline-flex;
  align-items: center;
  padding: 0 1rem;
  font-weight: 600;
  color: var(--text-muted);
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  margin-bottom: -1px; /* tabs sit on the bar's divider line */
  text-decoration: none;
}
.admin-nav a:hover {
  color: var(--link-hover);
  text-decoration: none;
}
.admin-nav a[aria-current="page"] {
  color: var(--heading);
  background: var(--page-bg);
  border-color: var(--divider);
  border-bottom: 1px solid var(--page-bg); /* punch through the divider */
  box-shadow: inset 0 2px 0 var(--accent); /* CI red accent, like the mode switch */
}
.admin-logout {
  margin: 0 0 0.4rem;
}

.table-wrap {
  overflow-x: auto;
}
.table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1.5rem;
}
.table th,
.table td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--divider);
  vertical-align: top;
}
.table th {
  font-weight: 600;
  color: var(--text);
}
/* Sortable column header (admin/_sort.html): the whole label is the link. */
.th-sort {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.th-sort:hover,
.th-sort:focus-visible {
  color: var(--link-hover);
}
.th-sort__icon {
  font-size: 0.75em;
  color: var(--text-muted);
}
.th-sort.is-active .th-sort__icon {
  color: var(--link);
}

.recent {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: grid;
  gap: 0.4rem;
}

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: end;
  margin-bottom: 1rem;
}
.filters .field {
  gap: 0.2rem;
}
.filters .field span {
  font-size: 0.85rem;
  color: var(--text-muted);
}

.entry-cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}
.entry-card {
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  padding: 0.75rem;
  display: grid;
  gap: 0.5rem;
}
.entry-card__thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.photo-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.thumb {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
/* Clickable thumbnail (opens the lightbox). */
.thumb-btn {
  padding: 0;
  border: 0;
  background: none;
  line-height: 0;
  cursor: zoom-in;
  border-radius: var(--radius);
}
.thumb-btn:hover .thumb,
.thumb-btn:focus-visible .thumb {
  border-color: var(--et-blue-dark);
}
.entry-card__body h3 {
  margin: 0;
}
/* Whole-card link (entries): the heading is the accessible link; its ::after
   covers the card so any click opens the entry, while the thumbnail buttons
   sit above it and still open the lightbox. */
.entry-card--link {
  position: relative;
}
.entry-card--link:hover {
  border-color: var(--et-blue-dark);
  background: var(--surface-bg);
}
.entry-card--link .thumb-btn,
.entry-card--link .entry-card__loc,
.entry-card--link .inline-form {
  position: relative;
  z-index: 1; /* keep clickable above the card's stretched link */
}
/* The card's delete button sits apart from the record's own text. */
.entry-card__body .inline-form {
  margin: 0.25rem 0 0;
}
.stretched-link {
  color: inherit;
  text-decoration: none;
}
.stretched-link:hover {
  color: inherit;
  text-decoration: underline;
}
.stretched-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}
.muted {
  color: var(--text-muted);
  font-size: 0.9rem;
}
/* Preserve the line breaks the installer entered (notes are kept verbatim). */
.protocol-note {
  white-space: pre-line;
  margin: 0.25rem 0;
}

.photo-manage {
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  padding: 0.75rem;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.9rem;
}
.photo-manage .thumb {
  width: 128px;
  height: 96px;
}
.photo-manage__controls {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 1 1 260px;
  min-width: 0;
}
.photo-manage__name {
  font-size: 0.85rem;
  word-break: break-all;
  color: var(--text-muted);
}
.photo-manage__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.photo-manage .inline-form {
  margin: 0;
}
.inline-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
  margin: 0 0 0.75rem;
}
details.danger summary {
  cursor: pointer;
  color: var(--danger-text);
  font-weight: 600;
}
.badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: var(--info-bg);
  color: var(--info-text);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap; /* keep "2 new" a pill, not a wrapped blob on narrow cells */
}
/* Record-type badge (Installation/Report): neutral, leads its line. */
.badge--kind {
  margin-left: 0;
  margin-right: 0.35rem;
  background: var(--surface-bg);
  color: var(--text-muted);
  border: 1px solid var(--divider);
}
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.row-actions .inline {
  margin: 0;
}
.export-panel {
  border: 1px solid var(--divider);
  border-radius: var(--radius);
  padding: 1rem;
  display: grid;
  gap: 0.9rem;
  max-width: 45rem;
}
.export-dates {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* ---------- Location picker suggestions ---------- */
/* Rendered in flow (below the input) instead of overlaying: on phones an
   overlay hides the very fields the installer fills in next. */
.suggest {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--control-bg);
  max-height: 16rem;
  overflow-y: auto;
}
/* Plain block layout inside the buttons - flex/grid INSIDE a <button> is
   ignored by some mobile browsers (Firefox Android), which collapsed the row
   height and let the detail line spill into the next item. */
.suggest__item {
  display: block;
  width: 100%;
  min-height: var(--tap-target);
  padding: 0.55rem 0.75rem;
  border: 0;
  border-bottom: 1px solid var(--divider);
  background: none;
  color: var(--text);
  font: inherit;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
}
.suggest__item:last-child {
  border-bottom: 0;
}
.suggest__item:hover,
.suggest__item:focus-visible {
  background: var(--surface-bg);
}
.suggest__item strong {
  display: block;
  color: var(--heading);
}
.suggest__item span {
  display: block;
  color: var(--text-muted);
  font-size: 0.85rem;
  overflow-wrap: anywhere;
}

/* ---------- Destructive actions ---------- */
/* Distinct from the solid-red primary CTA: an outline that fills on hover, so
   "Delete" never looks like the affirmative button next to it. */
.btn--danger {
  background: var(--control-bg);
  color: var(--danger-text);
  border-color: var(--danger-text);
}
.btn--danger:hover {
  background: var(--et-red);
  color: #fff;
  border-color: var(--et-red);
}
.danger-heading {
  color: var(--danger-text);
}

/* ---------- File-picker buttons ---------- */
/* A label styled as a button wraps a hidden file input; admin.js submits the
   form on change, so there is no stray native "no file chosen" text. */
.file-btn {
  cursor: pointer;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Photo lightbox ---------- */
.lightbox {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(94vw, 1000px);
  max-height: 94vh;
  overflow: visible;
}
.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.82);
}
.lightbox__frame {
  position: relative;
  display: inline-block;
  line-height: 0;
}
.lightbox__img {
  display: block;
  max-width: 100%;
  max-height: 88vh;
  border-radius: var(--radius);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.lightbox__close {
  position: absolute;
  top: -0.85rem;
  right: -0.85rem;
  width: 2.25rem;
  height: 2.25rem;
  border: 0;
  border-radius: 999px;
  background: #fff;
  color: var(--et-gray-80);
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}

/* ---------- Mode switch (installer page) ----------
   Radio group styled like the former tabs. Mode-specific form blocks carry
   data-mode="ap|protocol" and are toggled purely by CSS on the checked radio,
   so switching works without JavaScript. */
/* Segmented control (was quiet underline tabs that people glossed over):
   two equally big boxed halves; the active one is elevated and carries the
   CI-red bottom bar. */
.mode-switch {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  border: 0;
  margin: 0 0 1.25rem;
  padding: 0;
}
.mode-switch__opt {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-target);
  text-align: center;
  padding: 0.7rem 0.9rem;
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--text-muted);
  background: var(--surface-bg);
  border: 1px solid var(--border);
  border-bottom: 3px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.mode-switch__opt input {
  /* visually hidden but focusable, so the label styles carry the state */
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.mode-switch__opt:hover {
  color: var(--heading);
}
.mode-switch__opt:has(input:checked) {
  color: var(--heading);
  background: var(--control-bg);
  border-color: var(--heading);
  border-bottom: 3px solid var(--accent);
}
.mode-switch__opt:has(input:focus-visible) {
  outline: 3px solid var(--et-blue-dark);
  outline-offset: 2px;
}
/* Ordering group around the shared location code (see the template). Same grid
   gap as .form so wrapping the fields changes nothing visually. On the
   Protokoll tab the Meldungstyp and Notiz lead and the location code follows;
   everywhere else DOM order already is the wanted order. */
.mode-fields {
  display: grid;
  gap: 1.1rem;
}
.install-form:has(#mode-protocol:checked) .mode-fields > [data-leads-protocol] {
  order: -1; /* two of them: they keep their DOM order relative to each other */
}

/* Three tabs (Arrival | AP | Report): thirds once there is room, stacked
   below that - three labels side by side on a phone truncate to nothing. */
.mode-switch--three {
  grid-template-columns: 1fr;
}
@media (min-width: 30rem) {
  .mode-switch--three {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* Cross-mode nudge at the bottom of the AP form: "went wrong" cases belong
   in a Report, not in the note field. */
.mode-note {
  display: grid;
  gap: 0.4rem;
  padding: 0.75rem 1rem;
  background: var(--info-bg);
  color: var(--info-text);
  border-radius: var(--radius);
}
.mode-note p {
  margin: 0;
  white-space: pre-line; /* the i18n text breaks per sentence */
}
.mode-note__switch {
  width: max-content;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}

/* Guarded by @supports: a browser without :has() shows EVERY tab's blocks
   (usable, server-side validation still applies) instead of hiding all.
   data-mode holds one or more space-separated tab names, so ~= matches a block
   that belongs to several tabs (e.g. data-mode="ap protocol"). */
@supports selector(:has(*)) {
  .install-form [data-mode] {
    display: none;
  }
  .install-form:has(#mode-arrival:checked) [data-mode~="arrival"],
  .install-form:has(#mode-ap:checked) [data-mode~="ap"],
  .install-form:has(#mode-protocol:checked) [data-mode~="protocol"] {
    display: revert;
  }
}

/* ---------- Checkbox row ---------- */
.checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
}
.checkbox input {
  width: 1.1rem;
  height: 1.1rem;
}

/* ---------- List toolbar + pagination ---------- */
.list-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 0 0 1rem;
}
.list-toolbar--bottom {
  margin: 1rem 0 0;
  justify-content: flex-end;
}
.list-toolbar__count {
  color: var(--text-muted);
  font-size: 0.9rem;
}
.list-toolbar__right {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.list-toolbar .inline {
  margin: 0;
}
.pager {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.pager__page {
  color: var(--text-muted);
  font-size: 0.9rem;
  min-width: 3.5rem;
  text-align: center;
}
.btn--sm {
  min-height: 2rem;
  padding: 0.25rem 0.7rem;
  font-size: 0.95rem;
}
.btn.is-disabled {
  opacity: 0.45;
  cursor: default;
  pointer-events: none;
}

/* ---------- Overview export bar ---------- */
.overview-export {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0 0 1.5rem;
  padding: 0.75rem 1rem;
  background: var(--surface-bg);
  border: 1px solid var(--divider);
  border-radius: var(--radius);
}
.overview-export__label {
  font-weight: 600;
}
.overview-export .inline {
  margin: 0;
}
.overview-export__custom {
  flex-basis: 100%;
}
.overview-export__custom > summary {
  cursor: pointer;
  width: max-content;
}
.overview-export .export-panel {
  margin-top: 0.75rem;
}

/* ---------- Inline per-row location delete ---------- */
.loc-delete > summary {
  cursor: pointer;
  list-style: none;
}
.loc-delete > summary::-webkit-details-marker {
  display: none;
}
.loc-delete[open] {
  flex-basis: 100%;
}
.loc-delete__form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: flex-end; /* the labelled field is taller than the button */
  margin-top: 0.5rem;
}
/* The "type the code" field carries a visible label and hint (.field already
   stacks them), so it needs room inside the otherwise horizontal form. */
.loc-delete__field {
  min-width: 16rem;
}

/* ---------- Responsive ---------- */
@media (min-width: 40rem) {
  .cards {
    grid-template-columns: 1fr 1fr;
  }
  .site-footer__inner {
    text-align: left;
  }
  .page-title {
    font-size: 2.25rem;
  }
  .entry-cards {
    grid-template-columns: 1fr 1fr;
  }
  /* Sticky filter bar on wider screens only - on phones the stacked fields are
     too tall to pin without eating the viewport. */
  .filters {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--page-bg);
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--border);
  }
}

