/* =====================================================================
   Teknaf WMS — design tokens
   ===================================================================== */
:root {
  --brand-900:#053b32; --brand-800:#075045; --brand-700:#0b6b5b;
  --brand-600:#0f8a74; --brand-500:#14a88d; --brand-100:#d7f2ec; --brand-50:#eefaf7;
  --accent:#f59e0b; --accent-soft:#fef3c7;

  --ok:#16a34a;   --ok-soft:#dcfce7;
  --info:#0284c7; --info-soft:#e0f2fe;
  --warn:#d97706; --warn-soft:#fef3c7;
  --bad:#dc2626;  --bad-soft:#fee2e2;
  --muted-soft:#eef2f6;

  /* Categorical palette for charts and bar lists. Mirrored in window.palette
     (public/js/app.js) so a Chart.js dataset and a CSS bar agree on colour. */
  --c1:#0f8a74; --c2:#0284c7; --c3:#f59e0b; --c4:#7c3aed;
  --c5:#dc2626; --c6:#0891b2; --c7:#65a30d; --c8:#db2777;

  --bg:#f4f6f8; --surface:#ffffff; --surface-2:#f8fafc;
  --line:#e2e8f0; --line-strong:#cbd5e1;
  --text:#0f172a; --text-2:#475569; --text-3:#94a3b8;

  --radius:10px; --radius-sm:6px;
  --shadow:0 1px 2px rgba(15,23,42,.06), 0 1px 3px rgba(15,23,42,.04);
  --shadow-lg:0 10px 30px rgba(15,23,42,.12);
  --sidebar-w:248px;
  /* Tab bar height: 9px + 10px padding, a 21px icon, a 3px gap and an 11px
     label, plus its top border. The bars that stack above it and the shell's
     bottom padding are all derived from this, so they cannot drift apart. */
  --field-nav-h:54px;
  --field-safe-b:env(safe-area-inset-bottom, 0px);
  /* Noto Sans Bengali sits behind Inter in the same stack: Latin glyphs keep
     coming from Inter, Bengali ones fall through to Noto. */
  --font:"Inter", "Noto Sans Bengali", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono:"SFMono-Regular", ui-monospace, Menlo, Consolas, monospace;
}

*,*::before,*::after { box-sizing:border-box; }
html,body { margin:0; padding:0; }
body {
  font-family:var(--font); background:var(--bg); color:var(--text);
  font-size:14px; line-height:1.55; -webkit-font-smoothing:antialiased;
}
a { color:var(--brand-700); text-decoration:none; }
a:hover { text-decoration:underline; }

/* An underline is a cue for a link inside a sentence. On a card, a tile, a nav
   row or an icon button it just draws a line under unrelated text — several
   views had already worked around it with inline `text-decoration:none`. These
   selectors outrank `a:hover` on specificity, so the workarounds are no longer
   load-bearing and the rule holds everywhere by default. */
a.btn:hover, a.card:hover, a.kpi:hover, a.doc-card:hover, a.field__stop:hover,
a.msg-card:hover, a.nav__item:hover, a.field__big:hover, a.langswitch:hover,
a.field__bar-btn:hover, a.topbar__btn:hover, a.pick__item:hover, a.notif:hover,
a.msg-row:hover, a.sidebar__brand:hover, a.field__back:hover, a.stops__item:hover,
.field__nav a:hover, .field__drawer a:hover, .sidebar a:hover, .topbar a:hover,
.tabs a:hover, .pager a:hover, .pagination a:hover, .btn-group a:hover,
.page-head__actions a:hover, .card__head a:hover, .empty a:hover,
.kpi a:hover, .legend a:hover { text-decoration:none; }

/* This rule used to read `a:has(> .icon):hover { text-decoration:none }`, for
   "an anchor whose whole content is an icon must never be underlined". There is
   no such anchor in this application — every one of the sixty-seven anchors
   that contain an icon also contains words, because an icon on its own is not
   a link anybody can read. So the rule never did what it was written for, and
   what it did instead was take the hover underline off every link with a glyph
   in front of it: the hotline in the footer, the buttons on a contact card, the
   rows in the field app's drawer.

   The ones that genuinely should not underline are the ones that are not
   sentences — buttons, cards, nav rows — and they are already named above by
   class, which is the honest way to say it. */
h1,h2,h3,h4 { margin:0; font-weight:650; letter-spacing:-.01em; line-height:1.25; }
/* A heading that labels a run of cards rather than titling a card. Several views
   were hand-rolling this with inline font-size and margins, so no two matched. */
.section-title {
  font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.07em;
  color:var(--text-3); margin:22px 0 10px;
}
.section-title:first-child { margin-top:0; }
h1 { font-size:22px; } h2 { font-size:17px; } h3 { font-size:15px; }
/* Headings carry record data — a point name, a message subject — so a pasted
   URL or a long Bengali compound breaks rather than running out of the head. */
h1, h2, h3 { overflow-wrap:anywhere; }
p { margin:0 0 10px; }
small { font-size:12px; }
/* The `hidden` attribute only carries the user agent's `display:none`, which any
   class setting a display beats on specificity. Several components here do set
   one — .card is a flex column, .badge is inline-flex, the save confirmation is a
   grid — so `el.hidden = true` silently did nothing and the confirmation overlay
   sat on top of the capture form permanently. One rule, applied to everything,
   rather than a per-element patch each time it bites. */
[hidden] { display:none !important; }

code { font-family:var(--mono); font-size:12px; background:var(--surface-2);
  padding:1px 5px; border-radius:4px; border:1px solid var(--line);
  /* A code is one token. In a squeezed table column "CP-001" was breaking at
     the hyphen into two lines, which reads as two codes. */
  white-space:nowrap; }

/* ---------------- app shell ---------------- */
.app { display:flex; min-height:100vh; }

/* The brand and the language switcher are pinned; only the list of links
   scrolls. With every group expanded the nav is around 1,700px of content in an
   800px column, and when the whole sidebar was the scroller the brand scrolled
   away, the language switcher was unreachable without scrolling to the very
   bottom, and there was nothing on the dark ground to show a scrollbar existed —
   so the pages below the fold read as simply missing. */
.sidebar {
  width:var(--sidebar-w); flex:0 0 var(--sidebar-w);
  background:linear-gradient(180deg,var(--brand-900),var(--brand-800) 55%,#04302a);
  color:#cfe9e2; display:flex; flex-direction:column;
  position:sticky; top:0; height:100vh; overflow:hidden;
}
.sidebar__brand { flex:0 0 auto; }
/* Bangla's conjuncts and the matra line above the letters need more vertical
   room than Latin at the same size, or descenders clip inside buttons and pills. */
.lang-bn { --font:"Noto Sans Bengali", "Inter", -apple-system, "Segoe UI", Roboto, sans-serif; }
.lang-bn body,
.lang-bn .btn,
.lang-bn .nav__item,
.lang-bn .tbl th,
.lang-bn .tbl td { line-height:1.65; }
.lang-bn .badge,
.lang-bn .pill,
.lang-bn .nav__badge { line-height:1.5; padding-top:2px; padding-bottom:2px; }
.lang-bn .nav__label, .lang-bn .nav__sec { letter-spacing:0; }
/* Bengali digits are proportional in Noto Sans Bengali, so tabular-nums cannot
   line them up. Numeric cells stay right-aligned and unwrapped, which keeps the
   column edge straight even when the glyph widths differ. */
.lang-bn .kpi__label { min-height:3em; letter-spacing:0; }
/* And in English, for the same reason it was needed in Bangla: one tile in five
   has a label that wraps — "Points not on any round" against "Active rounds" —
   and without a floor under the label that tile's figure sits a line lower than
   the four beside it. Two lines' worth, which is every label this system has. */
.kpi__label { min-height:2.6em; }
/* Bengali digits are wider than Latin ones; numeric columns need the extra room. */
.lang-bn .tbl td.num, .lang-bn .tbl th.num { white-space:nowrap; }

/* ---------------- language switcher ---------------- */
.langswitch {
  display:inline-flex; align-items:center; gap:6px; text-decoration:none;
  font-size:12.5px; font-weight:600; border-radius:7px; padding:5px 9px;
  border:1px solid var(--line); color:var(--text-2); background:var(--surface);
  white-space:nowrap;
}
.langswitch:hover { border-color:var(--brand-500); color:var(--brand-700); }
.langswitch__icon { font-size:13px; opacity:.65; }
.langswitch--topbar { margin-right:4px; }
.langswitch--field {
  /* 27px tall was well under any thumb, and it sat beside a 36px round button
     looking like an afterthought. This is the control a Bangla speaker needs
     when a screen comes up in English, so it has to be as easy to hit as the
     menu next to it. */
  min-height:36px; padding:5px 9px; font-size:11.5px; background:transparent;
  border-color:rgba(255,255,255,.35); color:inherit;
}
.langswitch--auth { align-self:center; }
/* On a page with no bar to sit in (the signed-out error pages) it takes the
   top-right corner, where the topbar would have put it. */
.langswitch--blank { position:fixed; top:14px; right:14px; z-index:5; }
@media (max-width:640px) { .langswitch--topbar span:not(.langswitch__icon) { display:none; } }

.sidebar__brand {
  display:flex; gap:10px; align-items:center; padding:18px 18px 16px;
  border-bottom:1px solid rgba(255,255,255,.09);
}
.sidebar__logo {
  width:34px; height:34px; border-radius:9px; flex:0 0 34px;
  background:linear-gradient(135deg,var(--brand-500),var(--brand-700));
  display:grid; place-items:center; font-size:17px;
  box-shadow:0 2px 10px rgba(20,168,141,.35);
}
.sidebar__brand > div { min-width:0; }
.sidebar__title { color:#fff; font-weight:680; font-size:14px; line-height:1.2; }
.sidebar__sub { font-size:11px; color:#8fc4b8; letter-spacing:.03em; text-transform:uppercase; }
/* One line each, clipped with an ellipsis: the sidebar is a fixed width and a
   long project name or menu label was being cut mid-letter by its overflow. */
.sidebar__title, .sidebar__sub, .nav__text { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.nav__text { min-width:0; flex:1 1 auto; }

.nav {
  padding:10px 10px 24px; flex:1 1 auto;
  /* min-height:0 or a flex child refuses to shrink below its content and the
     overflow lands on the sidebar again. */
  min-height:0; overflow-y:auto; overscroll-behavior:contain;
  scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.22) transparent;
}
/* A visible track on a dark panel. The overlay scrollbar Chrome draws by default
   is a pale sliver that never appears until you are already scrolling, which is
   no use as the only sign that there is more menu below. */
.nav::-webkit-scrollbar { width:8px; }
.nav::-webkit-scrollbar-track { background:transparent; }
.nav::-webkit-scrollbar-thumb { background:rgba(255,255,255,.18); border-radius:8px; }
.nav::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,.32); }
.nav__group { margin-top:14px; }
.nav__label {
  font-size:10px; text-transform:uppercase; letter-spacing:.09em;
  color:#6ea99b; padding:6px 10px 5px; font-weight:650;
}
/* A section heading inside a menu group. It has to read as subordinate to the
   group's own label rather than as a second one of those, and at this size
   colour alone could not carry it — "EVERYDAY WORK" over "COLLECTION WORK" was
   two headings of near-equal weight and no visible hierarchy. So the section
   takes a rule running out to the right of its words: it reads as a divider
   with a name on it, which is what it is, and nothing else in the menu looks
   like that. */
.nav__sec {
  display:flex; align-items:center; gap:8px;
  font-size:9.5px; text-transform:uppercase; letter-spacing:.07em;
  color:#5f9589; font-weight:650; padding:9px 10px 4px;
}
.nav__sec::after {
  content:''; flex:1 1 auto; height:1px; background:rgba(255,255,255,.08);
}
.nav__item + .nav__sec { margin-top:4px; }
.nav__item {
  display:flex; align-items:center; gap:10px; padding:8px 10px; margin:1px 0;
  border-radius:7px; color:#cfe9e2; font-size:13.5px; font-weight:500;
  transition:background .12s, color .12s;
}
.nav__item:hover { background:rgba(255,255,255,.08); color:#fff; text-decoration:none; }
.nav__item.is-active { background:var(--brand-600); color:#fff; box-shadow:inset 0 0 0 1px rgba(255,255,255,.12); }
.nav__icon { width:17px; text-align:center; font-size:14px; opacity:.95; }
.nav__badge {
  margin-left:auto; background:var(--accent); color:#3b2500; font-size:10.5px;
  font-weight:700; border-radius:20px; padding:1px 7px;
}

.main { flex:1; min-width:0; display:flex; flex-direction:column; }

.topbar {
  height:58px; background:var(--surface); border-bottom:1px solid var(--line);
  display:flex; align-items:center; gap:14px; padding:0 20px;
  position:sticky; top:0; z-index:40;
}
.topbar__title { font-weight:650; font-size:15px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* flex:1, not flex:0 1 auto. With shrink-only and a flex:1 spacer beside it, the
   fixed-width buttons on the right win every pixel and the title collapses to
   nothing — on a 390px screen the page name disappeared entirely, so the one
   thing telling you where you are was the one thing not shown. Growing means it
   takes the leftover space and truncates with an ellipsis instead. */
.topbar__id { min-width:0; flex:1 1 auto; }
.topbar__crumb { color:var(--text-3); font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The bar is a fixed 58px: a name or organisation long enough to wrap would
   spill out under it, so each line is one line. */
.topbar__user { display:flex; align-items:center; gap:9px; color:inherit; flex:0 0 auto; }
.topbar__user .avatar { flex:0 0 auto; }
.topbar__user-meta { line-height:1.2; min-width:0; max-width:190px; }
.topbar__user-meta > div { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.topbar__spacer { flex:1; }
/* The search box. It takes what room the title leaves, floors at a width that
   fits a registration number, and on a phone becomes the icon button alone —
   the field app has its own search on every step. */
.topbar__search { position:relative; display:flex; align-items:center; flex:0 1 300px; min-width:160px; }
.topbar__search input[type=search] {
  height:34px; padding:0 34px 0 32px; font-size:13px; border-color:var(--line); border-radius:8px;
}
.topbar__search input[type=search]:focus { border-color:var(--brand-500); }
.topbar__search-icon { position:absolute; left:10px; top:0; height:34px; display:grid; place-items:center; color:var(--text-3); pointer-events:none; }
.topbar__search-key {
  position:absolute; right:8px; top:8px; height:18px; padding:0 5px; border-radius:4px;
  border:1px solid var(--line); background:var(--surface-2); color:var(--text-3);
  font:600 11px/16px inherit; font-family:inherit; pointer-events:none;
}
.topbar__search input:focus ~ .topbar__search-key { display:none; }
@media (max-width:720px) { .topbar__search { display:none; } }

/* ---- the search panel ----
   Wider than the box it hangs from: the box is 300px because the top bar has a
   title and five controls to fit beside it, and a suggestion is a page name, a
   section and sometimes a code. Anchored to the box's start edge and allowed to
   run past its end, which is over the empty middle of the bar rather than over
   anything. The z-index clears the sticky top bar (60) and the sidebar drawer;
   Leaflet's furniture cannot reach it because every map isolates itself. */
.suggest {
  position:absolute; z-index:70; inset-block-start:calc(100% + 6px); inset-inline-start:0;
  inline-size:max(100%, 420px); max-block-size:min(70vh, 520px); overflow-y:auto;
  background:var(--surface); border:1px solid var(--line); border-radius:10px;
  box-shadow:0 14px 38px rgba(15,23,42,.18); padding:5px;
}
@media (max-width:900px) { .suggest { inline-size:max(100%, 320px); } }
.suggest__group {
  padding:8px 9px 4px; font-size:10.5px; font-weight:700; letter-spacing:.04em;
  text-transform:uppercase; color:var(--text-3);
}
.suggest__row {
  display:flex; align-items:center; gap:10px; padding:7px 9px; border-radius:7px;
  color:inherit; text-decoration:none; cursor:pointer;
}
.suggest__row:hover { text-decoration:none; }
/* One highlight, driven by the keyboard. :hover is deliberately not a second
   one — with both, moving the mouse a pixel while arrowing down lights two rows
   and neither of them is the one Enter would open. */
.suggest__row.is-active { background:var(--brand-50); }
.suggest__row.is-active .suggest__title { color:var(--brand-700); }
.suggest__icon { display:grid; place-items:center; inline-size:22px; color:var(--text-3); flex:0 0 22px; }
.suggest__row.is-active .suggest__icon { color:var(--brand-600); }
.suggest__text { min-width:0; flex:1 1 auto; }
.suggest__title { display:block; font-size:13px; font-weight:600;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.suggest__title code { font-size:11.5px; background:var(--surface-2);
  padding:1px 4px; border-radius:4px; }
.suggest__title mark { background:var(--brand-100); color:inherit; padding:0 1px; border-radius:2px; }
.suggest__sub { display:block; font-size:11.5px; color:var(--text-3);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The return glyph says what Enter would do to the row under the highlight, so
   it shows on that row only. */
.suggest__go { font-size:12px; color:var(--text-3); opacity:0; flex:0 0 auto; }
.suggest__row.is-active .suggest__go { opacity:1; }
.suggest__empty { padding:14px 10px; font-size:12.5px; color:var(--text-3); text-align:center; }
.suggest__all { border-top:1px solid var(--line); border-radius:0 0 7px 7px; margin-top:4px; }
.suggest__all .suggest__title { font-weight:600; color:var(--text-2); }
/* The first-hour list on the help page: numbered steps that are links. */
.start-list { list-style:none; margin:0; padding:0; }
.start-step { display:flex; align-items:center; gap:14px; padding:12px 16px; border-bottom:1px solid var(--line); color:inherit; }
.start-list li:last-child .start-step { border-bottom:0; }
.start-step:hover { background:var(--brand-50); text-decoration:none; }
.start-step__n {
  flex:0 0 28px; width:28px; height:28px; border-radius:50%; display:grid; place-items:center;
  background:var(--brand-600); color:#fff; font-weight:700; font-size:13px;
}
.start-step__main { min-width:0; flex:1; display:flex; flex-direction:column; gap:2px; }
.start-step__title { font-weight:650; }
.start-step__blurb { font-size:12.5px; color:var(--text-2); }
.start-step__go { color:var(--text-3); font-size:16px; }
.start-step:hover .start-step__go { color:var(--brand-700); }
/* Results, grouped by register. */
.search-hit { display:flex; align-items:center; gap:12px; padding:10px 16px; border-bottom:1px solid var(--line); color:inherit; }
.search-hit:last-child { border-bottom:0; }
.search-hit:hover { background:var(--brand-50); text-decoration:none; }
.search-hit__icon { flex:0 0 auto; width:32px; height:32px; border-radius:9px; display:grid; place-items:center; background:var(--brand-50); color:var(--brand-700); }
.search-hit__main { min-width:0; flex:1; }
.search-hit__title { font-weight:600; }
.search-hit__sub { font-size:12px; color:var(--text-3); }
.search-hit mark { background:var(--accent-soft); color:inherit; border-radius:2px; padding:0 1px; }
.topbar__btn {
  position:relative; width:34px; height:34px; border-radius:8px; border:1px solid var(--line);
  background:var(--surface); display:grid; place-items:center; cursor:pointer; color:var(--text-2);
}
.topbar__btn:hover { background:var(--surface-2); }
.topbar__dot {
  position:absolute; top:-4px; right:-4px; min-width:16px; height:16px; padding:0 4px;
  border-radius:9px; background:var(--bad); color:#fff; font-size:10px; font-weight:700;
  display:grid; place-items:center;
}
.avatar {
  width:32px; height:32px; border-radius:50%; background:var(--brand-600); color:#fff;
  display:grid; place-items:center; font-weight:650; font-size:12.5px; flex:0 0 32px;
}
.avatar--sm { width:26px; height:26px; flex:0 0 26px; font-size:11px; }
/* The same circle, holding a photograph instead of an initial. object-fit is
   what keeps a portrait from being squashed into the square the width and
   height ask for. */
.avatar--img { object-fit:cover; background:var(--surface-2); padding:0; }

/* The picture a record already has, above the picker that would replace it. */
.photo-current {
  display:flex; align-items:center; gap:12px; margin-bottom:10px;
  padding:9px; border:1px solid var(--line); border-radius:var(--radius-sm);
  background:var(--surface-2);
}
.photo-current img {
  width:96px; height:96px; flex:0 0 96px; border-radius:10px; object-fit:cover;
  border:1px solid var(--line);
}

/* A person in a list row: initial, name, and the quieter identifiers under it.
   Email and phone as their own table columns pushed the columns that actually
   get scanned — role, ward, status — off the right edge. */
.who { display:flex; align-items:center; gap:9px; min-width:0; }
.who__text { min-width:0; }
.who__name { font-weight:600; display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.who__sub { font-size:11.5px; color:var(--text-3); overflow-wrap:anywhere; }

/* Centred in whatever width the main column has. Capped at 1560px and left-
   aligned, the page hugged the sidebar on a wide monitor and, the moment the
   sidebar was hidden to make room, jumped to the left edge and left the room
   it had made empty on the right. */
.content { padding:20px; max-width:1560px; width:100%; margin:0 auto; }

.page-head {
  display:flex; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-bottom:16px;
}
.page-head__text { flex:1; min-width:220px; }
.page-head__sub { color:var(--text-2); font-size:13px; margin-top:3px; }
.page-head__actions { display:flex; gap:8px; flex-wrap:wrap; }

/* ---------------- cards & grid ---------------- */
.card {
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); box-shadow:var(--shadow);
  /* Grid rows stretch every card to the height of the tallest one. Without a
     column layout the body keeps its own height and the extra space collects
     under it, so two charts side by side started at the same top edge but ended
     at different ones. As a flex column the body absorbs the slack instead. */
  display:flex; flex-direction:column; min-width:0;
}
.card__head {
  padding:13px 16px; border-bottom:1px solid var(--line);
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
}
.card__head h2, .card__head h3 { margin:0; }
.card__sub { color:var(--text-3); font-size:12px; }
/* Free text in a card — a note holding a pasted map link — breaks rather than
   running out of the card. break-word, not anywhere: it leaves intrinsic sizing
   alone, so nothing in a grid shrinks to fit a long word that would have fit. */
.card__body { padding:16px; flex:1 1 auto; min-height:0; overflow-wrap:break-word; }
/* Only bodies that actually hold a chart become flex containers, so no other
   card body has its children turned into flex items. */
.card__body:has(> .chart-box) { display:flex; flex-direction:column; }
.card__body--flush { padding:0; }
.card__foot { padding:10px 16px; border-top:1px solid var(--line); background:var(--surface-2);
  border-radius:0 0 var(--radius) var(--radius); font-size:12.5px; color:var(--text-2); }
/* A card that is asking to be acted on rather than read — the maintenance due
   list at the top of its page. A warm edge and a tinted head are enough; a full
   colour wash on a table makes the rows themselves harder to read. */
.card--attention { border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); }
.card--attention .card__head {
  background:color-mix(in srgb, var(--warn-soft) 55%, var(--surface));
  border-bottom-color:color-mix(in srgb, var(--warn) 30%, var(--line));
  border-radius:var(--radius) var(--radius) 0 0;
}

.grid { display:grid; gap:16px; }
/* 176, not 190: a five-tile row — the reports page, the maintenance page —
   needs 5x + 4 gaps to fit the ~990px of content beside the sidebar on a 1280
   screen, and at 190 it came to 1014 and dropped the fifth tile onto a row of
   its own. */
.grid--kpi   { grid-template-columns:repeat(auto-fit,minmax(176px,1fr)); }
.grid--2     { grid-template-columns:repeat(auto-fit,minmax(340px,1fr)); }
.grid--3     { grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); }
.grid--4     { grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); }
/* Cards whose width is set by what they contain rather than by a column count:
   the report library's tiles carry three actions in a row, and those actions
   need 335px of clear space in Bangla ("প্রিন্ট / পিডিএফ" is wider than
   "Print / PDF"). At the 260px of .grid--3 the third button dropped to a line
   of its own in every card, so every tile was 50px taller than it needed to be
   and the row of actions read as two ragged rows. Sized to the content, the
   library falls to two comfortable columns on a laptop and three or four on a
   wide screen, and the buttons stay on one line at every one of them. */
.grid--docs  { grid-template-columns:repeat(auto-fit,minmax(355px,1fr)); }
@media (max-width:520px) { .grid--docs { grid-template-columns:minmax(0,1fr); } }
/* auto-fit gives whatever fits, which for eight tiles on a wide screen is five
   and three. Where the count is known and even, say so. */
@media (min-width:900px) { .grid--fixed4 { grid-template-columns:repeat(4,minmax(0,1fr)); } }
/* Six of a thing — the actor types over the waste-actor register. Six divides by
   six, three and two; auto-fit gave four and then two, which is not the end of a
   list but a hole in the middle of the page with the last two tiles stranded
   beside it. */
.grid--six { grid-template-columns:repeat(2,minmax(0,1fr)); }
@media (min-width:760px)  { .grid--six { grid-template-columns:repeat(3,minmax(0,1fr)); } }
@media (min-width:1300px) { .grid--six { grid-template-columns:repeat(6,minmax(0,1fr)); } }

/* ---------------- a register drawn as cards ----------------
   The second way to read a register, beside the table. A table answers "how do
   these compare"; a grid of cards answers "which one is this" — and for the
   registers that carry a photograph (a scrap dealer's yard, a shopfront with no
   address that a map would recognise) that second question is most of the work.
   The classes are named for the job rather than for the actors register, so the
   fleet and the collection points can take the same view when they want it.

   The tray sits inside the register's own card, under the same filter bar and
   above the same pager, on a recessed ground so the cards read as contents
   rather than as a second layer of framing. */
.rec-tray { background:var(--surface-2); }
/* auto-fill, not auto-fit: on the last page of a filtered register there may be
   two records, and auto-fit would stretch two cards across the full width — a
   400px-wide photograph of one shop beside another, which reads as a different
   page rather than the end of this one. auto-fill keeps the column width and
   leaves the empty tracks empty. */
.rec-grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fill,minmax(258px,1fr)); }

.rec-card {
  position:relative; display:flex; flex-direction:column; min-width:0;
  background:var(--surface); border:1px solid var(--line);
  border-radius:var(--radius); overflow:hidden;
  transition:border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
.rec-card:hover { border-color:var(--brand-500); box-shadow:var(--shadow-lg); }
/* The keyboard path through the card is the name and the two foot buttons; the
   cover is aria-hidden and untabbable, so the ring has to be drawn on the card
   when anything inside it takes focus or the focused card is unfindable in a
   grid of fifteen. */
.rec-card:focus-within { border-color:var(--brand-600); box-shadow:0 0 0 3px var(--brand-100); }

/* 16:10 — tall enough that a frontage is more than a strip of roof, short
   enough that three cards still fit a laptop screen without scrolling. The
   fixed ratio also means a grid of mixed portrait and landscape photographs
   lines up, which is what makes the row scannable. */
.rec-card__cover {
  display:grid; place-items:center; aspect-ratio:16/10; background:var(--surface-2);
  border-bottom:1px solid var(--line); color:var(--text-3); overflow:hidden;
}
.rec-card__cover img { width:100%; height:100%; object-fit:cover; display:block; }
.rec-card__cover .icon { opacity:.55; }
/* The state that is actually common. Twelve of the fourteen partners in this
   register have no photograph, so a band sized for one was a 269x167 grey
   rectangle holding a 34px icon — nine tenths of it empty, repeated down the
   whole grid, with every name pushed a third of a screen lower than it needed to
   be. A crest instead: a third of the height, the tint the type's own, and the
   icon large enough to do the job it was put there for, which is telling a
   landfill from a scrap yard before reading a word. The 16:10 band above is
   still what a real photograph gets. */
.rec-card__cover--crest {
  aspect-ratio:auto; block-size:64px; place-items:center;
  background:
    radial-gradient(120% 140% at 50% 0%,
      color-mix(in srgb, var(--brand-600) 12%, transparent), transparent 70%),
    var(--surface-2);
}
.rec-card__cover--crest .icon { opacity:1; color:color-mix(in srgb, var(--brand-600) 72%, var(--text-3)); }
/* On the photograph, not under it: the status is the first thing that decides
   whether the rest of the card is worth reading, and a suspended partner should
   be obvious before the eye reaches the name. */
.rec-card__status {
  position:absolute; top:9px; right:9px; z-index:1;
  box-shadow:0 1px 3px rgba(15,23,42,.22);
}

.rec-card__body { padding:12px 13px; display:flex; flex-direction:column; gap:8px; flex:1 1 auto; min-width:0; }
.rec-card__title { margin:0; font-size:14.5px; font-weight:650; line-height:1.3; }
.rec-card__title a { color:var(--text); }
.rec-card__title a:hover { color:var(--brand-700); }
.rec-card__tags { display:flex; flex-wrap:wrap; align-items:center; gap:5px; }
.rec-card__licence { font-size:11px; color:var(--text-3); }

.rec-card__facts { list-style:none; margin:0; padding:0; display:grid; gap:6px; }
.rec-card__facts li { display:flex; gap:7px; align-items:flex-start; font-size:12.5px; color:var(--text-2); min-width:0; }
.rec-card__facts .icon { flex:0 0 auto; margin-top:2px; color:var(--text-3); }
.rec-card__fact { min-width:0; display:flex; flex-direction:column; }
/* Every line in the two-fact block clips rather than wraps. A service area is
   free text — "Wards 1–3, Teknaf Bazar, Jetty Road" — and letting it wrap makes
   one card in the row two lines taller than its neighbours, which in a grid is
   a ragged bottom edge on every card that follows it. The whole string is in
   the record, one click away. */
.rec-card__fact > * , .rec-card__fact { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rec-card__sub { font-size:11.5px; color:var(--text-3); }
.rec-card__tel { color:var(--text-3); font-size:11.5px; font-variant-numeric:tabular-nums; }
.rec-card__tel:hover { color:var(--brand-700); }
/* The chips are the one thing allowed to wrap: which streams a partner takes is
   the reason the recycling shops are being looked at at all. Capped at two rows
   by the slice in the view, not here.

   They follow the facts rather than being pushed to the floor of the body. A row
   of cards is as tall as the one with the most chips in it, and pinning the chips
   down there spent the difference as a gap in the middle of every other card —
   between the ward and the chips, where it read as a line that had failed to
   load. The slack now collects at the bottom of the body, against the tinted
   foot, where there is nothing to mistake it for. */
.rec-card__streams { padding-top:2px; }

.rec-card__foot {
  display:flex; align-items:center; gap:14px; padding:9px 13px;
  border-top:1px solid var(--line); background:var(--surface-2);
}
.rec-card__stat { min-width:0; }
.rec-card__stat-v { font-size:14px; font-weight:700; color:var(--text); line-height:1.15;
  font-variant-numeric:tabular-nums; }
.rec-card__stat-k { font-size:10px; text-transform:uppercase; letter-spacing:.05em; color:var(--text-3); }
.rec-card__acts { margin-left:auto; display:flex; gap:4px; }
.rec-card__acts .btn { padding-left:9px; padding-right:9px; }

/* ---- a record card with no photograph ----
   A round has no frontage to put at the top, so the status goes in the tag row
   beside the code rather than pinned to a cover that is not there, and the edge
   carries the warning the way the old list rows did. */
.rec-card--flag { border-inline-start:3px solid var(--warn); }
.rec-card--bad  { border-inline-start:3px solid var(--bad); }
.rec-card__fact.is-bad { color:var(--bad); font-weight:650; }
.rec-card__fact.is-empty { color:var(--text-3); }

/* The measurements, in a strip of their own between the body and the foot.
   Four numbers that mean something by comparison with the next card's four,
   which they cannot be if each sits at the end of a sentence in a paragraph:
   in a grid they line up down the column and a heavy round shows itself. */
.rec-card__nums {
  display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:6px;
  padding:9px 13px; border-top:1px solid var(--line);
}
.rec-card__stat.is-bad .rec-card__stat-v { color:var(--bad); }
.rec-card__flag {
  font-size:10px; font-weight:700; color:var(--bad);
  text-transform:uppercase; letter-spacing:.03em;
}

/* ---- when a round runs ----
   The one coloured thing on a record. "Is that daily or not" is the first
   question asked of a round, and the answer used to be a run of small grey day
   names under the start time — identical in weight to the round beside it that
   runs every day, so the two could only be told apart by reading both.

   Green every day, blue a set of days, red nothing planned at all. The words
   are printed in every case: a colour that has to be learned is not an
   improvement on text, it only makes the text findable. */
.runs {
  display:inline-flex; align-items:center; flex-wrap:wrap; gap:2px 6px;
  min-width:0; max-width:100%; padding:3px 9px; border-radius:999px;
  border:1px solid transparent; font-size:11.5px; font-weight:650; line-height:1.5;
}
.runs .icon { flex:0 0 auto; opacity:.75; }
.runs__label { white-space:nowrap; }
.runs__sub, .runs__n { font-weight:500; opacity:.8; white-space:nowrap; }
.runs__sub::before, .runs__n::before { content:'·'; margin-inline-end:5px; opacity:.55; }
.runs--daily { background:var(--ok-soft);   color:#166534; border-color:#bbf7d0; }
.runs--some  { background:var(--info-soft); color:#075985; border-color:#bae6fd; }
.runs--none  { background:var(--bad-soft);  color:#991b1b; border-color:#fecaca; }
/* In a card it is a band on its own line; in a dense row it is one item among
   several and must not stretch to fill the line. */
.rec-card__body > .runs { align-self:flex-start; }
.rec-row > .runs { flex:0 0 auto; }
/* The rounds' compact rows and the unrouted points now share one set of column
   tracks; see .rec-rows--grid below. */

/* ---- the compact reading of the same records ----
   One line each, for somebody who already knows the rounds and wants forty of
   them at once. Deliberately not a table: a round carries four measurements, a
   timetable, a van and a person, and as twelve columns two of them were behind
   a horizontal scrollbar on a 1440px desktop. Here each group keeps its own
   width, the numbers carry their unit instead of a column heading, and the line
   wraps as a whole on a narrow screen — so nothing is ever off the edge. */
.rec-rows { list-style:none; margin:0; padding:0; }
.rec-row {
  display:flex; align-items:center; flex-wrap:wrap; gap:6px 14px;
  padding:9px 16px; border-bottom:1px solid var(--line); font-size:12.5px;
}
.rec-row:last-child { border-bottom:0; }
.rec-row:hover { background:var(--brand-50); }
/* An inset shadow rather than a border, so a flagged row's text starts at the
   same left edge as every other row's. As a border it was three pixels in, and
   three pixels is exactly enough to see that a column is not a column. */
.rec-row--flag { box-shadow:inset 3px 0 0 var(--warn); }
.rec-row--bad  { box-shadow:inset 3px 0 0 var(--bad); }
.rec-row__id { flex:1 1 190px; min-width:0; display:flex; flex-direction:column; }
.rec-row__id a { font-weight:650; font-size:13px; color:var(--text); line-height:1.3; }
/* The name wraps; it does not clip. At 1024px the column is about 150px and
   fourteen of these rounds ended in an ellipsis — "Secondary transfer …" tells
   a planner nothing the code above it did not. A row one line taller costs
   nothing here: the columns are set by the grid, so they stay in line whatever
   height a row takes. */
.rec-rows--grid .rec-row__id a { overflow-wrap:anywhere; }
.rec-row__id a:hover { color:var(--brand-700); }
.rec-row__code { font-size:10.5px; color:var(--text-3); font-variant-numeric:tabular-nums; }
.rec-row__when { flex:0 1 auto; color:var(--text-2); white-space:nowrap; }
.rec-row__ward {
  /* Wide enough for "Ward 6 — Teknaf Bazar", which is how a ward is named here:
     at 110px every one of them ended in an ellipsis, and a column that always
     clips is a column that says nothing. */
  flex:0 1 170px; min-width:0; color:var(--text-2);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.rec-row__nums {
  display:flex; gap:12px; flex:0 0 auto;
  font-weight:650; font-variant-numeric:tabular-nums;
}
.rec-row__nums small { font-weight:500; font-size:10.5px; color:var(--text-3); }
.rec-row__nums .is-bad { color:var(--bad); }
.rec-row__who { display:flex; flex-direction:column; flex:0 1 135px; min-width:0; line-height:1.35; }
.rec-row__who > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rec-row__who .is-bad { color:var(--bad); font-weight:650; }
.rec-row__who .is-empty { color:var(--text-3); }
.rec-row__acts { margin-inline-start:auto; display:flex; gap:5px; }
/* Below the width the column grid needs, the line stops being a line and
   becomes a block: name, when it runs, the ward against its figures, who is on
   it, then the buttons. Every part starts at the same left edge and the figures
   end at the same right one, so the rows still read down the page.

   This picks up exactly where the grid leaves off. In between the two there
   used to be a flex-wrap zone — 760 to 1000 — where the row wrapped onto two
   ragged lines with nothing above anything, and at 900px it overflowed the
   page sideways. There is no in-between now. */
@media (max-width:999px) {
  .rec-row { gap:5px 10px; padding:10px 14px; }
  .rec-row__id, .rec-row__when, .rec-row__who, .rec-row__acts { flex:1 1 100%; }
  .rec-row__ward { flex:1 1 auto; }
  /* Pushed to the right edge on every row, so a column of figures is still a
     column even when the rest of the row has stacked. */
  .rec-row__nums { margin-inline-start:auto; }
  .rec-row__acts { display:flex; }
  .rec-row__acts .btn { flex:1 1 auto; justify-content:center; }
}

/* ---- the compact reading, as real columns ----
   Flex dropped each row's items wherever they fell. That reads as a list at
   1440px and as two ragged lines at 1024, with nothing above anything and the
   ward starting at a different place on every row.

   So the grid is on the list rather than on the row: the rows are
   `display:contents`, every cell of every row shares one set of column tracks,
   and a name in row four sits under the name in row one. The price is that a
   row may never add or drop a cell — a missing one pulls every column after it
   out of line — so both lists here emit all of theirs, empty where they must be.

   Below this width the rows go back to being blocks, which is what a phone
   wants; see the flex rules above. */
@media (min-width:1000px) {
  .rec-rows--grid { display:grid; align-items:center; }
  /* Every flexible track carries a floor. A `max-content` column never gives
     ground, so with plain `minmax(0,1fr)` beside one the name column went to a
     single letter at 1024px — "E", "S", "B" down the list — while the chip kept
     every pixel it asked for. The chip's own track is `auto` and the chip
     wraps, so it is the one that yields first. */
  .rec-rows--rounds {
    grid-template-columns:
      minmax(150px,1.4fr)  /* name and code */
      minmax(0,auto)       /* when it runs */
      minmax(90px,.9fr)    /* ward */
      max-content          /* the measurements */
      minmax(120px,.8fr)   /* van and crew lead */
      max-content;         /* open / edit */
  }
  .rec-rows--points {
    grid-template-columns:
      minmax(170px,1.8fr) minmax(0,auto) minmax(90px,.8fr) minmax(130px,1fr) max-content;
  }
  .rec-rows--grid .rec-row { display:contents; }
  /* The rule and the hover live on the cells, because a display:contents row
     draws no box of its own. No column gap: each cell carries its own padding,
     so the rule underneath runs unbroken instead of dashing across the row. */
  .rec-rows--grid .rec-row > * {
    min-width:0; padding:9px 7px; border-bottom:1px solid var(--line);
  }
  .rec-rows--grid .rec-row > *:first-child { padding-inline-start:16px; }
  .rec-rows--grid .rec-row > *:last-child { padding-inline-end:16px; }
  .rec-rows--grid .rec-row:last-child > * { border-bottom:0; }
  .rec-rows--grid .rec-row:hover > * { background:var(--brand-50); }
  /* The warning edge as an inset shadow rather than a border: a border would
     push the first cell's text three pixels out of the column it shares with
     every other row, which is the alignment this whole block is for. */
  .rec-rows--grid .rec-row--flag > *:first-child { box-shadow:inset 3px 0 0 var(--warn); }
  .rec-rows--grid .rec-row--bad  > *:first-child { box-shadow:inset 3px 0 0 var(--bad); }
  .rec-rows--grid .rec-row__nums,
  .rec-rows--grid .rec-row__acts { justify-content:flex-end; }
  .rec-rows--grid .rec-row__when { display:flex; }
}
/* The code line carries the status beside it now, so the two sit on one
   baseline rather than the badge riding above the text. */
.rec-row__code { display:inline-flex; align-items:center; gap:6px; }

/* ---- table / cards switch ----
   Two links dressed as one control, at the end of the filter row. Links rather
   than buttons because the view is in the URL: the reader can bookmark the
   cards, and the back button undoes a switch the way it undoes a filter. */
.view-switch {
  display:inline-flex; margin-left:auto; align-self:flex-end;
  border:1px solid var(--line-strong); border-radius:var(--radius-sm);
  background:var(--surface); overflow:hidden; height:var(--control-h);
}
.view-switch__btn {
  display:inline-flex; align-items:center; gap:6px; padding:0 11px;
  font-size:12.5px; font-weight:600; color:var(--text-2); background:var(--surface);
}
.view-switch__btn + .view-switch__btn { border-left:1px solid var(--line); }
.view-switch__btn:hover { background:var(--surface-2); color:var(--text); text-decoration:none; }
.view-switch__btn.is-active { background:var(--brand-600); color:#fff; }
.view-switch__btn.is-active:hover { background:var(--brand-700); color:#fff; }
.view-switch .icon { flex:0 0 auto; }
/* Beside a wrapped row of filters, two labelled buttons take a line of their
   own on a handset. The icons carry it from there; the accessible name stays in
   the title and the group's own label. */
@media (max-width:700px) {
  .view-switch { margin-left:0; }
  .view-switch__label { position:absolute; width:1px; height:1px; padding:0; margin:-1px;
    overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
  .view-switch__btn { padding:0 13px; }
}
@media (max-width:560px) {
  .rec-grid { grid-template-columns:minmax(0,1fr); gap:12px; }
  .rec-tray { padding:12px; }
}
/* A filter bar folded behind its own summary. Five controls stacked open push
   the records they filter off a handset screen entirely; at a desk they are
   open and cost nothing. The count says how many are on, so a folded bar can
   never hide the reason a list looks short. */
.filter-fold { border-bottom:1px solid var(--line); }
.filter-fold > summary {
  display:flex; align-items:center; gap:8px; cursor:pointer;
  padding:11px 16px; font-size:13px; font-weight:650; color:var(--text-2);
  background:var(--surface-2); border-radius:var(--radius) var(--radius) 0 0;
}
.filter-fold > summary::-webkit-details-marker { display:none; }
.filter-fold > summary::after {
  content:'▾'; margin-inline-start:auto; color:var(--text-3); transition:transform .15s;
}
.filter-fold[open] > summary::after { transform:rotate(180deg); }
.filter-fold > .filters { padding:14px 16px; }
@media (prefers-reduced-motion:reduce) { .filter-fold > summary::after { transition:none; } }

/* ---------------- a search box, and the refinements behind it ----------------
   A register's filter row treats every control as equally likely, so the search
   box — the one people reach for most — sat fifth in a fold that opens closed.
   Here the box is always out and the dropdowns are one line below it, which also
   reports how many of them are narrowing the list. */
/* Bottom-aligned: the sort control carries a label above its box and the
   search box does not, so centring put the two boxes at different heights and
   left the buttons floating between them. Every control shares --control-h. */
.fsearch__bar { display:flex; gap:8px; align-items:flex-end; padding:14px 16px 0; flex-wrap:wrap; }
.fsearch__bar > .field { flex:0 0 190px; margin:0; }
.fsearch__bar > .btn { height:var(--control-h); }
.fsearch__box { position:relative; flex:1 1 280px; min-width:0; }
/* Room for the glyph, which is decoration the caret must not run into. The
   attribute selector is load-bearing: the shared control rule that sets every
   box's padding and height is further down the file and would otherwise win. */
.fsearch__box input[type=search] { padding-inline-start:35px; height:var(--control-h); }
.fsearch__icon {
  position:absolute; inset-inline-start:11px; top:50%; transform:translateY(-50%);
  display:grid; place-items:center; color:var(--text-3); pointer-events:none;
}
.fsearch__hint { margin:7px 16px 0; font-size:11.5px; color:var(--text-3); line-height:1.45; }
/* Inside a card that already has a search bar above it, the fold is a divider
   with a label rather than the top of the card. */
.filter-fold--inline { margin-top:12px; }
.filter-fold--inline > summary { border-radius:0; border-top:1px solid var(--line); }
@media (max-width:640px) {
  .fsearch__bar .btn { flex:1 1 auto; justify-content:center; }
  /* Six controls open on a handset is six rows to scroll past before the rounds
     themselves. Two abreast makes it three, which fits above the fold the way
     the collapsed version used to — without hiding anything. */
  .filter-fold--inline .filters .field { flex:1 1 calc(50% - 5px); min-width:0; max-width:none; }
  .filter-fold--inline .filters .btn-group { flex:1 1 100%; }
}

.grid--sidebar { grid-template-columns:minmax(0,2fr) minmax(0,1fr); }
@media (max-width:1100px){ .grid--sidebar { grid-template-columns:minmax(0,1fr); } }

/* ---------------- KPI tiles ---------------- */
/* Tiles in a row are equal height, so the three moving parts are pinned:
   the label reserves two lines (Bangla labels wrap where English ones do not),
   the value sits directly under it, and the meta line is pushed to the floor of
   the tile. Without that a wrapped label pushed one tile's figure out of line
   with its neighbours. */
.kpi {
  --tone:var(--brand-500); --tone-soft:var(--brand-50);
  position:relative; overflow:hidden; padding:17px 18px 16px; display:flex; flex-direction:column;
  /* A diagonal wash across the whole tile tinted the text as well as the card,
     and five of them in a row read as five coloured slabs. The tint is now a
     corner glow behind the icon: the hue still identifies the tile at a glance,
     the figure sits on clean white, and the ink keeps its contrast. */
  background:
    radial-gradient(120% 100% at 100% 0%,
      color-mix(in srgb, var(--tone) 13%, transparent), transparent 62%),
    var(--surface);
  border-color:color-mix(in srgb, var(--tone) 20%, var(--line));
}
.kpi--ok   { --tone:var(--ok);   --tone-soft:var(--ok-soft); }
.kpi--info { --tone:var(--info); --tone-soft:var(--info-soft); }
.kpi--warn { --tone:var(--warn); --tone-soft:var(--warn-soft); }
.kpi--bad  { --tone:var(--bad);  --tone-soft:var(--bad-soft); }
/* Not a verdict: "how long we take" is a measure, and the house colour says so
   where ok/warn/bad would turn a number into a judgement of itself. */
.kpi--brand { --tone:var(--brand-600); --tone-soft:var(--brand-50); }
/* The label is the quietest thing on the tile and the figure the loudest —
   they were competing at 11.5px bold slate against 24px bold. */
.kpi__label { font-size:11px; text-transform:uppercase; letter-spacing:.06em;
  color:var(--text-3); font-weight:700; line-height:1.35; min-height:2.7em;
  /* clears the absolutely positioned icon in the top-right corner */
  padding-right:46px; }
/* The figure steps down a little on a narrow desktop rather than clipping:
   a lakh in Bengali digits with its unit is one unbreakable token, and the
   tile hides its overflow. */
.kpi__value { font-size:clamp(25px, 2.5vw, 31px); font-weight:750; letter-spacing:-.03em; margin-top:5px; line-height:1.06;
  font-variant-numeric:tabular-nums; min-width:0; overflow-wrap:anywhere; }
/* Two lines at most — one actor handling eight streams was making its whole
   row of tiles taller. The full text is in the title attribute where it can
   be longer. */
.kpi__meta { font-size:11.5px; color:var(--text-2); margin-top:auto; padding-top:8px; line-height:1.45;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; overflow-wrap:anywhere; }
/* Solid, so the tile keeps its colour, but without the coloured drop shadow
   under it — five glowing chips in a row was the loudest thing on the page. */
.kpi__icon {
  position:absolute; right:14px; top:14px; width:36px; height:36px; border-radius:10px;
  display:grid; place-items:center; font-size:16px; line-height:1;
  background:var(--tone); color:#fff;
}

/* A tinted top edge and a matching value colour, so a row of tiles reads as a
   set of distinct measures at a glance rather than five identical grey boxes. */
/* The edge and the icon both read --tone, so a new tone modifier only has to
   set that one variable instead of repeating itself in four places. */
.kpi::after {
  content:''; position:absolute; inset:0 0 auto 0; height:3px;
  background:var(--tone);
}
.kpi__value { color:var(--tone); }
.kpi--ok   .kpi__value { color:#15803d; }
.kpi--info .kpi__value { color:#0369a1; }
.kpi--warn .kpi__value { color:#b45309; }
.kpi--bad  .kpi__value { color:#b91c1c; }
.kpi--brand .kpi__value { color:var(--brand-800); }
.kpi:not([class*="kpi--"]) .kpi__value { color:var(--brand-800); }

/* Counting tiles take the categorical palette rather than the ok/warn/bad
   vocabulary: eight registers are eight different things, not eight verdicts,
   and a red "audit entries" tile would read as an alarm rather than a count.
   The soft ground is mixed from the tone so each tile only names its hue once. */
.kpi--c1 { --tone:var(--c1); } .kpi--c2 { --tone:var(--c2); }
.kpi--c3 { --tone:var(--c3); } .kpi--c4 { --tone:var(--c4); }
.kpi--c5 { --tone:var(--c5); } .kpi--c6 { --tone:var(--c6); }
.kpi--c7 { --tone:var(--c7); } .kpi--c8 { --tone:var(--c8); }
/* The ninth register in a set of eight hues: a log of everything else. */
.kpi--slate { --tone:#64748b; }
.kpi--c1, .kpi--c2, .kpi--c3, .kpi--c4,
.kpi--c5, .kpi--c6, .kpi--c7, .kpi--c8, .kpi--slate {
  --tone-soft:color-mix(in srgb, var(--tone) 13%, var(--surface));
}
/* Amber and lime carry the tile's edge and icon well but fail contrast as text
   on white, so the figure itself is the same hue darkened toward ink. */
.kpi--c1 .kpi__value, .kpi--c2 .kpi__value, .kpi--c3 .kpi__value, .kpi--c4 .kpi__value,
.kpi--c5 .kpi__value, .kpi--c6 .kpi__value, .kpi--c7 .kpi__value, .kpi--c8 .kpi__value,
.kpi--slate .kpi__value {
  color:color-mix(in srgb, var(--tone) 72%, #0b1220);
}

/* A tile that is a link to a register rather than a measurement: the count is
   the whole content, so it sits on the floor of the tile with the label above
   it. On the full-height tile the figure floated near the top with 60px of
   nothing beneath it, and a wrapped label in Bangla knocked it out of line with
   its neighbours — anchored to the bottom, all eight figures share a baseline
   whichever language is on. */
.kpi.kpi--count { min-height:112px; padding:16px; }
.kpi--count .kpi__value { font-size:26px; margin-top:auto; padding-right:22px; }

/* A tile that leads somewhere says so: it lifts, and its arrow appears. */
a.kpi { transition:transform .14s ease, box-shadow .14s ease, border-color .14s ease; }
a.kpi:hover {
  transform:translateY(-2px); border-color:var(--tone);
  box-shadow:0 8px 22px color-mix(in srgb, var(--tone) 22%, transparent);
}
a.kpi:focus-visible { outline:2px solid var(--brand-500); outline-offset:2px; }
.kpi__go {
  position:absolute; right:13px; bottom:11px; font-size:14px; color:var(--text-3);
  opacity:0; transform:translateX(-3px); transition:opacity .14s ease, transform .14s ease;
}
a.kpi:hover .kpi__go { opacity:1; transform:none; }
/* A tile whose page this reader cannot open keeps its <a> but loses the href:
   it should not lift or offer an arrow to a 403. */
a.kpi:not([href]):hover { transform:none; border-color:var(--line); box-shadow:var(--shadow); }
a.kpi:not([href]) .kpi__go { display:none; }
@media (prefers-reduced-motion:reduce) {
  a.kpi, .kpi__go { transition:none; }
  a.kpi:hover { transform:none; }
}

/* ---------------- tables ---------------- */
.table-wrap { overflow-x:auto; position:relative; }
/* position:relative so the wrapper is the containing block of anything
   absolutely positioned inside the table — the .sr-only labels in action
   columns above all. Without it they were positioned against a box outside the
   scroller, escaped its clipping, and widened the whole page on a phone
   (683px on /handsets, 635px on a role's grid) even though nothing visible
   stuck out. */
/* A table with many columns squeezes rather than scrolls: the cells are allowed
   to wrap anywhere (see the responsive safety net below), so ten columns in
   990px became ten 99px columns and a name broke to one word per line. The wrap
   is already a scroller — the table just never asked for the width that would
   make it scroll.

   Counted in CSS rather than tagged in each view, so a column added to a list
   tomorrow is handled without anybody remembering to. Each floor is roughly
   what that many columns need before the cells start breaking mid-word. */
.table-wrap > .tbl:has(thead th:nth-child(6))  { min-width:760px; }
.table-wrap > .tbl:has(thead th:nth-child(8))  { min-width:950px; }
.table-wrap > .tbl:has(thead th:nth-child(10)) { min-width:1180px; }
.table-wrap > .tbl:has(thead th:nth-child(12)) { min-width:1360px; }
/* The field app is a phone screen with its own layout; nothing there scrolls
   sideways on purpose. */
.fieldapp .table-wrap > .tbl { min-width:0; }
table.tbl { width:100%; border-collapse:collapse; font-size:13px; }
.tbl th {
  text-align:left; padding:9px 14px; background:var(--surface-2);
  border-bottom:1px solid var(--line); font-size:11px; letter-spacing:.05em;
  text-transform:uppercase; color:var(--text-2); font-weight:650; white-space:nowrap;
}
.tbl td { padding:10px 14px; border-bottom:1px solid var(--line); vertical-align:middle; }
.tbl tbody tr:last-child td { border-bottom:0; }
.tbl tbody tr:hover { background:var(--brand-50); }
.tbl .num { text-align:right; font-variant-numeric:tabular-nums; }
/* A wide table scrolls sideways inside its card, and the column that scrolled
   out of sight first was the last one — Edit, View, Complete. On a 1366px
   laptop seven of the eleven registers hid their actions behind a scrollbar
   nobody knew was there. app.js marks a table whose last heading is empty and
   which is wider than its wrap; that table keeps its actions pinned to the
   right edge, over the columns sliding underneath, with a shadow that says so. */
.tbl--sticky-actions th:last-child, .tbl--sticky-actions td:last-child {
  position:sticky; right:0; background:var(--surface); z-index:1;
  box-shadow:-10px 0 10px -10px rgba(15,23,42,.22);
}
.tbl--sticky-actions th:last-child { background:var(--surface-2); z-index:2; }
/* An action whose label carries a name — "Assign to Abdul Aktar" — must not
   widen the pinned column over the data it is pinned beside. */
.tbl td:last-child .btn { max-width:240px; }
.tbl--sticky-actions tbody tr:hover td:last-child { background:var(--brand-50); }
/* ...but not on a phone. Pinning the actions costs a fixed 150–200px of an
   already narrow screen, and the pinned column is drawn *over* the row: on a
   390px handset the collection log's point name arrived as "Co / 5 - / CP /
   Tek" down the left of the buttons, which is not a name, it is wreckage.
   Unpinned, the row reads properly and the buttons are one swipe away — and
   the row itself opens the record wherever `data-href` is set. */
@media (max-width:640px) {
  .tbl--sticky-actions th:last-child, .tbl--sticky-actions td:last-child {
    position:static; box-shadow:none;
  }
}
.tbl .nowrap { white-space:nowrap; }
/* A row that opens its record on click (app.js, data-href). The hand cursor
   is the only hint it needs: the hover tint is already there. */
.tbl tbody tr[data-href] { cursor:pointer; }
/* A 44px picture beside the name in a register — the record's cover photo, or
   the register's own icon while it has none. Deliberately small: a register is
   read as a list of rows, and the row that identifies a record is its name and
   code. The photograph is here to be recognised in passing; the size that lets
   you study it is on the record's own page, and full size is one click away in
   the lightbox. */
.tbl__thumb {
  display:grid; place-items:center; width:44px; height:44px; border-radius:8px; overflow:hidden;
  background:var(--surface-2); border:1px solid var(--line); color:var(--text-3);
}
.tbl__thumb img { width:100%; height:100%; object-fit:cover; display:block; }
.tbl__thumb:hover { border-color:var(--brand-500); }
.sr-only {
  position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
.tbl__empty { padding:36px 16px; text-align:center; color:var(--text-3); }

/* An activity list on a record page grows with the record: a point collected
   from twice a day has seven hundred rows within a year, and the card carrying
   them grew until it was the page — a metre of table beside a right-hand column
   that ran out after three cards, with the point's own details stranded at the
   top of a screen nobody scrolls back to.

   So the list keeps its own scroll and the card stays a card. The headings are
   pinned inside it, because a quantity halfway down a scrolled list with the
   column names gone is a number with no name on it. */
.table-scroll { max-height:min(52vh,440px); overflow:auto; }
.table-scroll .tbl thead th { position:sticky; top:0; z-index:2; }
/* The pinned row sits over cells that scroll beneath it, so it needs its own
   opaque ground and an edge — the border-bottom on a sticky cell is painted
   with the cell and scrolls away with the rest of the border-collapse. */
.table-scroll .tbl thead th { background:var(--surface-2); box-shadow:inset 0 -1px 0 var(--line); }
.table-scroll .tbl { min-width:0; }

/* ---------------- "needs attention" cards ----------------
   The dashboard's alert strip was a line of small links with a count pill each:
   four different urgencies reading as one grey sentence, and on a phone it
   wrapped into an unreadable run. As cards they are scannable at a glance and
   each one is a tap target, with the colour carrying the urgency — red for work
   already late, amber for a complaint waiting, blue for what is merely due. */
/* Same column template as .grid--kpi, so the alert row and the KPI row below it
   line up column for column instead of being two independent rhythms — and the
   same card height, so the two rows read as one grid of eight cards rather than
   two bands of different sizes. */
.alert-card, .kpi { min-height:152px; }
.alerts {
  display:grid; gap:16px; margin-bottom:16px;
  grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));
}
.alert-card {
  --tone:var(--warn); --tone-soft:var(--warn-soft);
  position:relative; display:flex; gap:13px; align-items:flex-start;
  padding:16px 18px; border-radius:var(--radius); color:inherit;
  /* The same corner wash the KPI tiles below now use, so the two rows read as
     one grid rather than as an older band sitting above a newer one. */
  background:
    radial-gradient(120% 100% at 100% 0%,
      color-mix(in srgb, var(--tone) 14%, transparent), transparent 62%),
    var(--surface);
  border:1px solid color-mix(in srgb, var(--tone) 22%, var(--line));
  border-left:4px solid var(--tone);
  box-shadow:var(--shadow); transition:transform .12s, box-shadow .12s, border-color .12s;
}
.alert-card:hover {
  transform:translateY(-2px); text-decoration:none;
  box-shadow:0 8px 22px color-mix(in srgb, var(--tone) 22%, transparent);
}
.alert-card__icon {
  flex:0 0 auto; width:38px; height:38px; border-radius:10px;
  display:grid; place-items:center; background:var(--tone); color:#fff;
}
.alert-card__body { min-width:0; }
.alert-card__n {
  font-size:27px; font-weight:750; line-height:1.05; color:var(--tone);
  letter-spacing:-.03em; font-variant-numeric:tabular-nums;
}
.alert-card__label { font-size:13px; font-weight:650; color:var(--text); margin-top:2px; }
.alert-card__hint { font-size:11.5px; color:var(--text-2); margin-top:2px; line-height:1.45; }
.alert-card--bad  { --tone:var(--bad);  --tone-soft:var(--bad-soft); }
.alert-card--warn { --tone:var(--warn); --tone-soft:var(--warn-soft); }
.alert-card--info { --tone:var(--info); --tone-soft:var(--info-soft); }
.alert-card--ok   { --tone:var(--ok);   --tone-soft:var(--ok-soft); }

/* A second line under a cell's own value — what was done when a maintenance
   job was closed. Clamped, with the whole note in the title attribute, so one
   long sentence cannot stretch the column. */
.tbl__note {
  font-size:11px; color:var(--text-3); line-height:1.4; margin-top:2px;
  max-width:220px; white-space:normal;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}

/* A waste category, drawn the way the category itself says it should be: its
   own colour, its own icon. The four categories were a hardcoded colour map in
   the field app and an icon alias table in services/icons.js; both now come off
   the row, so a category added this afternoon looks like the ones that shipped. */
.cat-chip {
  display:inline-flex; align-items:center; gap:5px; padding:2px 9px; border-radius:20px;
  font-size:11.5px; font-weight:600; line-height:1.7; white-space:nowrap;
  /* A tag name is user-entered and may be long. The chip is capped so it
     cannot dictate the width of a table column, and the name inside clips
     with an ellipsis (on its own span — an inline-flex box draws none on its
     bare text). Letting it wrap instead was tried: in a squeezed table the
     column collapsed to one letter per line. */
  max-width:min(100%, 260px);
  color:color-mix(in srgb, var(--cat, #64748b) 78%, #000);
  background:color-mix(in srgb, var(--cat, #64748b) 14%, var(--surface));
  border:1px solid color-mix(in srgb, var(--cat, #64748b) 28%, transparent);
}
.cat-chip .icon { width:14px; height:14px; flex:0 0 auto; }
.cat-chip__text { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Several chips in one table cell — the tags a waste stream carries. They wrap
   rather than widening the column, because a stream may carry four. */
.chip-row { display:flex; flex-wrap:wrap; gap:4px; }

/* ---------------- badges & pills ---------------- */
.badge {
  /* inline-block, not inline-flex: an ellipsis is only drawn on a block's own
     text, and a status label that outgrows its cell — Bengali labels run
     long — clips with one rather than pushing the column wider. */
  display:inline-block; vertical-align:middle; padding:2px 9px; border-radius:20px;
  font-size:11.5px; font-weight:600; line-height:1.7; white-space:nowrap;
  max-width:100%; overflow:hidden; text-overflow:ellipsis;
  background:var(--muted-soft); color:var(--text-2);
}
.badge--ok   { background:var(--ok-soft);   color:#15803d; }
.badge--info { background:var(--info-soft); color:#0369a1; }
.badge--warn { background:var(--warn-soft); color:#b45309; }
.badge--bad  { background:var(--bad-soft);  color:#b91c1c; }
.badge--muted{ background:var(--muted-soft);color:var(--text-2); }
.badge--brand{ background:var(--brand-100); color:var(--brand-800); }
.badge--sm   { font-size:10.5px; padding:1px 7px; }
.badge__dot { width:6px; height:6px; border-radius:50%; background:currentColor; }

.chip {
  display:inline-block; padding:1px 8px; border-radius:5px; font-size:11.5px;
  background:var(--surface-2); border:1px solid var(--line); color:var(--text-2); margin:2px 3px 2px 0;
  max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:middle;
}
/* Quick windows beside a date-range filter: one tap for the common answer, the
   two date fields for every other one. `is-on` marks the window in force. */
.range-presets { display:flex; flex-wrap:wrap; gap:2px; align-items:center; margin-left:auto; }
a.chip { padding:5px 11px; font-size:12px; }
a.chip:hover { border-color:var(--brand-500); color:var(--brand-700); text-decoration:none; }
a.chip.is-on {
  background:var(--brand-600); border-color:var(--brand-600); color:#fff; font-weight:600;
}

/* ---------------- buttons ---------------- */
/* A button is exactly one control tall, whether it is a <button> or an <a>.
   Sized by padding, the two differed by the line-height each tag inherits — a
   link takes the page's 1.55, a button the browser's "normal" — so "Save" stood
   33px beside a 38px "Cancel", and "Complete" 26px beside a 31px "Edit", on
   every form and in every table. The height is set outright and the label is
   centred in it, so the tag stops mattering. */
.btn {
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  min-height:var(--control-h); padding:0 14px; line-height:1.2;
  border-radius:var(--radius-sm); border:1px solid var(--line-strong);
  background:var(--surface); color:var(--text); font-size:13px; font-weight:600;
  cursor:pointer; font-family:inherit; transition:background .12s, border-color .12s, box-shadow .12s;
  white-space:nowrap;
}
.btn:hover { background:var(--surface-2); text-decoration:none; }
.btn--primary { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
.btn--primary:hover { background:var(--brand-700); border-color:var(--brand-700); }
.btn--danger { background:var(--bad); border-color:var(--bad); color:#fff; }
.btn--danger:hover { background:#b91c1c; }
/* The secondary button. It used to have no border at all, which made "Edit"
   in a table and "Cancel" beside a green "Save" read as plain text — a link at
   best, a label at worst — and a reader who is not sure what is clickable does
   not click. A faint outline says "button" without competing with the primary
   one beside it; hover and keyboard focus firm it up. */
.btn--ghost { background:transparent; border-color:var(--line); color:var(--text-2); }
.btn--ghost:hover { background:var(--surface-2); border-color:var(--line-strong); color:var(--text); }
.btn:focus-visible { outline:none; border-color:var(--brand-500); box-shadow:0 0 0 3px rgba(20,168,141,.16); }
/* In a table the small ghost is a row action: the brand colour marks it as
   the thing to press, and the pill shape keeps twenty of them in a column
   from reading as twenty underlined words. */
.tbl .btn--sm.btn--ghost { color:var(--brand-700); border-color:var(--brand-100); background:var(--surface); border-radius:999px; }
.tbl .btn--sm.btn--ghost:hover { background:var(--brand-50); border-color:var(--brand-500); color:var(--brand-800); }
.btn--sm { min-height:28px; padding:0 10px; font-size:12px; }
.btn--block { width:100%; }
.btn[disabled] { opacity:.5; cursor:not-allowed; }

.btn-group { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }

/* A column of navigation actions inside a card. A stack of .btn--block centres
   every label, so five links of five different lengths read as a ragged column
   with no common left edge; here the icon chip, the label and the chevron each
   have a fixed lane. */
.action-list { display:grid; gap:8px; }
.action-list .btn { justify-content:flex-start; text-align:left; padding:10px 12px; gap:10px; }
.action-list .btn::after {
  content:'\203A'; margin-left:auto; padding-left:8px;
  color:var(--text-3); font-size:17px; line-height:1;
}
.action-list .btn--primary::after { color:rgba(255,255,255,.7); }
.action-list__icon {
  flex:0 0 auto; width:27px; height:27px; border-radius:7px; display:grid;
  place-items:center; font-size:14px; background:var(--brand-50); color:var(--brand-700);
}
.action-list .btn--primary .action-list__icon { background:rgba(255,255,255,.2); color:#fff; }

/* ---------------- forms ---------------- */
/* A sentence above a form saying what filling it in will do. Non-obvious forms
   were shipping with only their field labels to explain themselves. */
.form-lede { margin:0 0 14px; font-size:12.5px; color:var(--text-2); line-height:1.5; }

/* The answers that are usually right by default, folded away. The cover form
   asks seven questions and four of them are almost always the same; the fold is
   what makes the common case short without removing the uncommon one. */
.field-more { grid-column:1/-1; }
.field-more > summary {
  cursor:pointer; font-size:12.5px; font-weight:650; color:var(--brand-700);
  padding:2px 0; width:max-content;
}
.field-more > summary::-webkit-details-marker { display:none; }
.field-more > summary::before { content:'+ '; }
.field-more[open] > summary::before { content:'– '; }
.field-more > .field { margin-top:12px; }

/* What a screen is FOR, as buttons at the top. Both continuity forms live at the
   foot of the page — three screens down on a handset — so the supervisor who
   came to record that somebody is away had to scroll past every consequence of
   their being away before they could say so. */
.quick-actions { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; }
.quick-actions .btn { flex:1 1 auto; justify-content:center; }
@media (min-width:641px) { .quick-actions .btn { flex:0 0 auto; } }

/* A closure that has already passed stays on the list as a record, greyed. */
.recs__item.is-past { opacity:.58; }

.form-grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); }
/* Cards fill the page, but a form does not become more readable at eight columns
   across — cap the density and let the fields breathe instead. `.field--full`
   still spans the row, whatever the count. */
@media (min-width:1180px) { .form-grid { grid-template-columns:repeat(4,minmax(0,1fr)); } }
@media (min-width:1500px) { .form-grid { grid-template-columns:repeat(5,minmax(0,1fr)); } }
/* Seven fields do not divide into four columns, and the leftover has to sit
   somewhere. Left to auto-placement it landed as a hole in the top-right of the
   card — three controls, a gap, then a full row of four underneath, which reads
   as an unfinished row rather than a deliberate shape. Widening the collection
   point, the one field here whose options are long enough to want the room,
   closes the first row exactly: point, time, planned pickup over vehicle, crew,
   actor, status. The column count is pinned at four for the same reason — at
   five, both rows end in a hole again. */
@media (min-width:1180px) {
  .form-grid--event, .form-grid--event[class] { grid-template-columns:repeat(4,minmax(0,1fr)); }
  .form-grid--event > .field--point { grid-column:span 2; }
}
/* A form that stays one field per line whatever the container width — the
   sidebar forms on the continuity screen, where two columns of 110px controls
   would be worse than one column of readable ones. After the width-based rules
   above, which are what it is overriding. */
.form-grid--stack, .form-grid--stack[class] { grid-template-columns:minmax(0,1fr); }
/* Except a date range, which is one question and reads as one row. */
.form-grid--stack .field-row { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.field { display:flex; flex-direction:column; gap:5px; min-width:0; }
.field--full { grid-column:1/-1; }
/* Half a row rather than all of it: an email address or a long reference is
   unreadable in a quarter-width box at four columns, and silly at full width. */
.field--wide { grid-column:span 2; }
@media (max-width:560px) { .field--wide { grid-column:1/-1; } }
.field label { font-size:12.5px; font-weight:600; color:var(--text-2); }
.field .hint { font-size:11.5px; color:var(--text-3); }
/* Every single-line control is exactly --control-h tall. Sized by padding they
   were three heights in one row — a text box 33px, a date picker 35.5px, a
   Select2 box 36px, the colour swatch 34px — and a form reads as slightly
   broken without anyone being able to say why. Text areas keep their own
   height; everything else, Select2 included, takes this one. */
:root { --control-h:36px; }
/* `url` is in this list because a box for an address is a text box like any
   other, and leaving it out is invisible until somebody adds one: it then
   draws in the browser's own hand — a thinner border, a different height, the
   text at a different inset — beside boxes that are all in this stylesheet's.
   Which is what the map's address and the film's address looked like on the
   public-page editor, and the links grid after them. */
input[type=text],input[type=email],input[type=password],input[type=number],input[type=date],
input[type=time],input[type=datetime-local],input[type=search],input[type=tel],input[type=url],
select,textarea {
  width:100%; padding:8px 11px; border:1px solid var(--line-strong); border-radius:var(--radius-sm);
  font-family:inherit; font-size:13.5px; background:var(--surface); color:var(--text);
  transition:border-color .12s, box-shadow .12s;
}
input[type=text],input[type=email],input[type=password],input[type=number],input[type=date],
input[type=time],input[type=datetime-local],input[type=search],input[type=tel],input[type=url],
select {
  height:var(--control-h); padding-top:0; padding-bottom:0;
}
input:focus,select:focus,textarea:focus {
  outline:none; border-color:var(--brand-500); box-shadow:0 0 0 3px rgba(20,168,141,.16);
}
/* The browser's own placeholder grey (#757575) is not a colour in this palette,
   and Select2 was already drawing its placeholder in --text-3 — so a text box
   and the dropdown beside it disagreed about what "empty" looks like. */
input::placeholder, textarea::placeholder { color:var(--text-3); opacity:1; }
textarea { resize:vertical; min-height:78px; }
input[type=color] { width:46px; height:var(--control-h); padding:2px; border:1px solid var(--line-strong);
  border-radius:var(--radius-sm); background:var(--surface); }
/* "optional" beside a label. A field that looks like every other field reads as
   required — and somebody who does not know their van's number, or does not
   want to leave a phone number, then has nothing safe to do. */
.field__optional {
  font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.04em;
  color:var(--text-3); margin-left:6px;
}
.check { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--text-2); }
/* A bare checkbox dropped into a form grid has no label above it, so it sat at
   the top of its cell while the inputs beside it sat under theirs. It lines up
   with the controls on its row instead. */
.form-grid > .check { align-self:end; min-height:var(--control-h); }
.check input { width:15px; height:15px; accent-color:var(--brand-600); }
.checks { display:flex; flex-wrap:wrap; gap:6px 14px; }

.filters {
  display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;
}
.filters .field { min-width:150px; flex:0 1 auto; max-width:min(100%, 320px); }
/* A filter box takes the width of its content, and the content of a dropdown
   is whatever is picked in it — one long ward name was stretching the box
   across the whole row. Capped, except the search field, which is asked to
   grow. */
.filters > .field[style*="flex:1"] { max-width:none; }

/* ---------------- misc ---------------- */
.flash-stack { position:fixed; top:14px; right:16px; z-index:200; display:flex;
  flex-direction:column; gap:8px; max-width:380px; }
.flash {
  padding:11px 14px; border-radius:var(--radius); box-shadow:var(--shadow-lg);
  font-size:13px; display:flex; gap:9px; align-items:flex-start; background:var(--surface);
  border:1px solid var(--line); border-left-width:4px;
}
.flash--success { border-left-color:var(--ok); }
.flash--error   { border-left-color:var(--bad); }
.flash--info    { border-left-color:var(--info); }
.flash__close { margin-left:auto; cursor:pointer; color:var(--text-3); background:none; border:0; font-size:15px; }

.empty { padding:44px 20px; text-align:center; color:var(--text-3); }
.empty__icon { font-size:30px; margin-bottom:8px; opacity:.6; }

/* ---------------- pagination ---------------- */
.pager {
  display:flex; gap:14px; align-items:center; flex-wrap:wrap;
  padding:11px 14px; border-top:1px solid var(--line); background:var(--surface-2);
}
.pager__info { font-size:12.5px; color:var(--text-2); }
.pager__info strong { color:var(--text); font-variant-numeric:tabular-nums; }
.pager__size, .pager__jump { display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--text-3); }
/* The pager's own controls are a size down from a form's — the row is a
   footer, not a form — and the same size as each other. `width:auto` on the
   rows select undoes the global 100%, which on a phone stretched it across the
   whole card. */
.pager__size select {
  width:auto; height:30px; padding:0 24px 0 8px; font-size:12.5px; border-radius:var(--radius-sm);
  border:1px solid var(--line); background:var(--surface); color:var(--text);
}
.pager__jump { margin-left:auto; }
.pager__jump input { width:70px; height:30px; padding:0 8px; font-size:12.5px;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--surface); }
.pager .btn--sm { min-height:30px; }
/* nowrap: wrapping put the » on a line of its own on a phone. The strip scrolls
   sideways instead, and the first/last jumps are dropped where there is no
   room — Prev / Next and the numbers are the controls that matter. */
.pagination { display:flex; gap:5px; align-items:center; flex-wrap:nowrap; margin-left:auto; max-width:100%; overflow-x:auto; }
@media (max-width:560px) { .pagination a[aria-label] { display:none; } }
.pager__jump + .pagination, .pagination + .pager__jump { margin-left:14px; }
.pagination a, .pagination span {
  display:inline-flex; align-items:center; justify-content:center; min-width:32px; height:30px; text-align:center;
  padding:0 9px; border-radius:var(--radius-sm); border:1px solid var(--line);
  font-size:12.5px; background:var(--surface); color:var(--text-2);
  font-variant-numeric:tabular-nums;
}
.pagination a:hover { border-color:var(--brand-500); color:var(--brand-700); text-decoration:none; }
.pagination .is-current { background:var(--brand-600); border-color:var(--brand-600); color:#fff; font-weight:600; }
.pagination .pagination__gap { border:0; background:transparent; min-width:0; padding:5px 2px; }
.pagination .is-disabled { opacity:.4; pointer-events:none; }
@media (max-width:820px) {
  .pager { flex-direction:column; align-items:stretch; }
  .pagination, .pager__jump { margin-left:0; justify-content:center; }
}

/* ---------------- mobile shell ---------------- */
/* The sidebar is a flex item at desktop widths. Below 820px it leaves the flow
   entirely (position:fixed) so .main gets the full viewport, and is opened by the
   topbar menu button. Field crews are the primary phone users of this system. */
/* Desktop: the sidebar is part of the layout and the toggle hides it, giving
   the content the full window. Not `display:none` on the aside alone — the
   sidebar is a flex item with a fixed basis, so it has to stop taking part in
   the layout entirely for .main to reclaim the space. */
.app--nav-hidden > .sidebar { display:none; }

/* `.topbar .topbar__menu`, not `.topbar__menu`. The button also carries
   `.topbar__btn`, and `.nav__icon, .sidebar__logo, .topbar__btn { display:grid }`
   further down this file matched it at equal specificity and later — so the
   hamburger appeared on every desktop page, beside an already-open sidebar, and
   pressing it dimmed the whole window with the off-canvas scrim and locked
   scrolling with no drawer to show for it. Two classes settles it. */
/* Visible at every width now: below 820px it opens the off-canvas drawer, above
   it hides and shows the sidebar. It used to be mobile-only, which left desktop
   with no way to reclaim the 248px at all. */
.topbar .topbar__menu { display:grid; }
.nav-scrim { position:fixed; inset:0; background:rgba(2,6,23,.45); z-index:55; border:0; padding:0; }

@media (max-width:820px) {
  .sidebar {
    position:fixed; left:0; top:0; z-index:60; height:100vh;
    transform:translateX(-100%); transition:transform .22s ease;
  }
  .sidebar.is-open { transform:none; box-shadow:0 18px 44px rgba(0,0,0,.38); }
  .topbar { padding:0 12px; gap:10px; }
  .topbar__title { font-size:14px; }
  .content { padding:14px; }
  /* auto-fit minima exceed a phone's content width; one column instead */
  .grid--2, .grid--3, .grid--4 { grid-template-columns:minmax(0,1fr); }
  /* Figures stay two abreast. One tile per row was four screens of scrolling on
     a handset before the list they summarise came into view — every KPI row got
     that treatment by hand or did without it, so it is the default now.
     `--kpi-2up` remains for rows built on another grid. */
  .grid--kpi, .grid--kpi-2up { grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
  .grid--kpi .kpi, .grid--kpi-2up .kpi { padding:12px; }
  .grid--kpi .kpi__label, .grid--kpi-2up .kpi__label { padding-right:0; min-height:2.6em; }
  .grid--kpi .kpi__value, .grid--kpi-2up .kpi__value { font-size:21px; }
  .grid--kpi .kpi__icon, .grid--kpi-2up .kpi__icon { display:none; }
  .alerts { grid-template-columns:minmax(0,1fr); }
  /* One card per row is as wide as the screen, so the height that keeps eight
     cards matching on a desktop grid is just empty space here. */
  .alert-card, .kpi { min-height:0; }
  /* Counts are short enough to stay two-up rather than becoming eight
     full-width bands the reader has to scroll past to reach the page. */
  .grid--counts { grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
  .page-head__actions { width:100%; }
  /* The topbar carries a title, a crumb, a Reports link, two icon buttons, the
     user's name and role and a sign-out button. At 390px they overlap each other
     and run off the right edge. Everything reachable from the drawer or the
     profile page is dropped here; the avatar stays as the link to it. */
  /* .topbar prefix so these outrank `.btn { display:inline-flex }`, which sits
     later in this file at the same specificity and was quietly winning — the
     Reports button never hid on a phone and ate a third of the bar. */
  .topbar .topbar__crumb,
  .topbar .topbar__user-meta,
  /* The title is now the flexible element, so the spacer has nothing to do and
     would only compete with it for the same leftover pixels. */
  .topbar .topbar__spacer { display:none; }
}
/* Between the phone layout above and a comfortable desktop, the sidebar is
   still a fixed 248px and the bar's contents are not: from about 840 to 960px
   the sign-out button ran up to sixty pixels off the right edge, which put a
   horizontal scrollbar under every screen in that band — the bar is sticky, so
   it was the whole page that scrolled sideways. The name and role go first,
   because the avatar beside them is the same link to the same page. */
@media (min-width:821px) and (max-width:1000px) {
  .topbar .topbar__user-meta { display:none; }
}

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

/* =====================================================================
   Select2 — one widget, three surfaces
   The closed control in the form, the panel that floats under it, and on a
   phone the sheet that rises from the bottom edge. Everything below is
   written against Select2 4.1's markup and the classes app.js adds:

     .select2--panel          on the wrapper Select2 floats on <body>
     .select2--sheet          on that wrapper while it is a bottom sheet
     .select2-dropdown--sheet on the panel inside it
     .select2--field          on the panel, inside the field app (thumb rows)

   The control itself carries no class of its own: Select2 4.1 dropped the
   containerCssClass option, so the field-app control is keyed on .fieldapp
   above it. Every height in the control is derived from --control-h, and the
   field app and coarse-pointer profiles below set only that variable.

   Long values are the whole design problem here. A collection point is
   "Kitchen Market 3 — Teknaf Sadar", and the control it sits in is 150px
   wide. The rules: the closed control is always one line and clips (its
   title attribute carries the full text); the open panel is as wide as it
   needs to be, up to a cap, and every row wraps in full — nothing in the list
   is ever hidden behind an ellipsis; a code travels in its own chip so the
   part that tells two rows apart is never the part that gets clipped.
   ===================================================================== */

/* ---- the closed control ------------------------------------------------ */
.select2-container { max-width:100%; }
/* The wrapper Select2 floats on <body> for the open panel is also a
   .select2-container. app.js tags it .select2--panel, so the full width meant
   for the control in a form never reaches the panel. */
.select2-container:not(.select2--panel) { width:100% !important; }
.field .select2-container { display:block; }
/* A filter bar wraps on a phone; a 160px floor there forces a horizontal
   scrollbar, so the floor only applies once there is room for it. */
.filters .select2-container { min-width:0; }
@media (min-width:560px) { .filters .select2-container { min-width:160px; } }

.select2-container--default .select2-selection--single,
.select2-container--default .select2-selection--multiple {
  /* --line-strong, as the inputs have: on --line the dropdowns read a shade
     fainter than the text boxes beside them, which looked like a disabled state. */
  min-height:var(--control-h); border:1px solid var(--line-strong); border-radius:var(--radius-sm);
  background:var(--surface); font-size:13.5px; color:var(--text);
}
.select2-container--default .select2-selection--single { height:var(--control-h); }
.select2-container--default .select2-selection--single .select2-selection__rendered {
  line-height:calc(var(--control-h) - 2px); padding-left:11px; padding-right:34px; color:var(--text);
  /* One clipped line, whatever was picked: choosing a long name never reflows
     the form around the control. The full text is in the title attribute. */
  display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder { color:var(--text-3); }
/* The clear button. Select2 4.1 puts it *before* the rendered text as a
   right float, so the text box shrinks to make room for it. The vendor gives
   it a 26px height (so it sat above the centre line of a 36px control), Arial,
   and a 20px margin that left the x touching the chevron. It is now as tall as
   the control, centres its glyph, and clears the chevron's 20px box; the text
   beside it gives up the padding it no longer needs, since the float already
   keeps the two apart. */
.select2-container--default .select2-selection--single .select2-selection__clear {
  float:right; display:flex; align-items:center; height:calc(var(--control-h) - 2px);
  margin:0 24px 0 0; padding:0 4px; border:0; background:transparent; cursor:pointer;
  font-family:inherit; font-size:16px; font-weight:600; line-height:1; color:var(--text-3);
}
.select2-container--default .select2-selection--single .select2-selection__clear:hover,
.select2-container--default .select2-selection--single .select2-selection__clear:focus-visible { color:var(--text); outline:0; }
.select2-container--default .select2-selection--single .select2-selection__clear + .select2-selection__rendered { padding-right:6px; }
.select2-container--default .select2-selection--single .select2-selection__arrow { height:calc(var(--control-h) - 2px); right:4px; }
/* The chevron is a CSS-border triangle the vendor paints #888; it takes the
   secondary text colour instead, and flips to the same colour when open rather
   than the vendor's darker one. */
.select2-container--default .select2-selection--single .select2-selection__arrow b { border-top-color:var(--text-2); }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { border-bottom-color:var(--text-2); }
.select2-container--default.select2-container--focus .select2-selection--single,
.select2-container--default.select2-container--focus .select2-selection--multiple,
.select2-container--default.select2-container--open .select2-selection--single,
.select2-container--default.select2-container--open .select2-selection--multiple {
  border-color:var(--brand-500); box-shadow:0 0 0 3px rgba(20,168,141,.16); outline:0;
}

/* Multi-select. Two things in Select2 4.1's markup have to be undone before the
   chips can be restyled at all.

   The remove button is a <button> the vendor positions absolutely at the chip's
   left edge, and pays for with `padding-left:20px` on the chip. Restyling the
   chip without carrying that padding over drops the x on top of the first
   letter of the name. Rather than keep two numbers in step, the chip becomes a
   flex row and the button goes back into the flow, after the label where a tag
   normally carries it.

   The inline search box is a <textarea>, so the app's own textarea rule —
   width:100%, min-height:78px, padding, border — was landing on it. That is
   what turned a 36px control into a 250px box with a second box drawn inside
   it. Its width is set inline by Select2 (full width while the placeholder is
   showing, text-width once something is picked), so only the rest is reset.

   One row of chips is exactly one control tall. In Select2 4.1's markup the
   search box is a sibling of the chip list, not a child of it, so the wrapping
   happens on the box: the list takes the width of its chips, the search takes
   what is left of the line, and either drops to the next line when this one is
   full. A multi-select grows downward, never sideways. */
.select2-container--default .select2-selection--multiple {
  padding:3px 6px; cursor:text; display:flex; flex-wrap:wrap; align-items:center; gap:4px;
  min-height:var(--control-h); max-width:100%; box-sizing:border-box;
}
/* The chip list is a <ul> between the control and its chips. As a box of its
   own it measured the width of all its chips on one line, took the whole line
   when they overflowed it, wrapped them inside itself, and pushed the search
   box down to a line of its own — three chips read as three rows. With no box
   the chips and the search box are items of the same flex row and wrap
   together: the search takes what is left of the last chip line. */
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
  display:contents; margin:0; padding:0;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
  display:inline-flex; align-items:center; max-width:100%; min-width:0; height:24px;
  margin:0; padding:0 4px 0 10px; border-radius:999px;
  background:var(--brand-50); border:1px solid var(--brand-100); color:var(--brand-800);
  /* line-height 1 clipped the ascenders of Bengali names; the chip has a fixed
     height and centres its label, so the leading costs nothing. */
  font-size:12px; line-height:1.5; font-family:inherit;
}
/* A long tag name clips inside its chip rather than widening it past the
   control; the chip's title attribute carries the whole name. */
.select2-container--default .select2-selection--multiple .select2-selection__choice__display {
  padding:0; min-width:0; flex:0 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
  order:2; position:static; flex:0 0 auto; margin:0 0 0 4px; padding:0 3px; min-width:16px;
  border:0; border-radius:999px; background:transparent;
  /* The vendor sets Arial on the button; the × is drawn in the app's face. */
  color:var(--brand-700); font-family:inherit; font-size:14px; font-weight:700; line-height:1;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus {
  background:var(--brand-100); color:var(--brand-900); outline:0;
}
.select2-container--default .select2-selection--multiple .select2-search--inline {
  flex:1 1 90px; min-width:90px; display:flex; align-items:center;
}
.select2-container--default .select2-search--inline .select2-search__field,
.select2-container--default .select2-search--inline .select2-search__field:focus {
  /* Select2 sets the width inline from the text typed; the wrapper is the
     flex item that decides the room, so the box simply fills it. */
  width:100% !important; min-height:0; height:24px; line-height:24px; margin:0; padding:0 2px;
  border:0; background:transparent; box-shadow:none; resize:none;
  /* A <textarea> the vendor resets to the browser's sans-serif: a Bengali
     name typed here fell back out of Noto Sans Bengali mid-form. */
  font-family:inherit; font-size:13.5px; color:var(--text);
}
.select2-container--default .select2-search--inline .select2-search__field::placeholder { color:var(--text-3); opacity:1; }

/* ---- the row --------------------------------------------------------------
   One layout for an option wherever it is drawn:

     [swatch] [CODE] main text
                     quiet second line

   data-swatch is a colour dot (waste streams, destinations), data-code a
   monospace chip that never wraps or clips, data-sub a second line shown in
   the list only, so the closed control stays one line tall. app.js builds
   this from the option's attributes; a plain option is plain text. */
.s2row { display:flex; align-items:center; gap:8px; min-width:0; max-width:100%; }
.s2row__swatch { width:12px; height:12px; flex:0 0 12px; border-radius:50%; box-shadow:0 0 0 1px rgba(15,23,42,.12); }
.s2row__code {
  flex:0 0 auto; font-family:var(--mono); font-size:11.5px; line-height:1.5; padding:0 5px;
  border-radius:4px; background:var(--surface-2); border:1px solid var(--line); color:var(--text-2);
  white-space:nowrap;
}
.s2row__text { display:flex; flex-direction:column; min-width:0; line-height:1.35; }
.s2row__main { min-width:0; }
.s2row__sub { font-size:11.5px; color:var(--text-3); }
/* In the list the text wraps in full — a name that takes two lines takes two
   lines — and the dot and code sit on the first of them. */
.select2-results__option .s2row { align-items:flex-start; width:100%; }
.select2-results__option .s2row__swatch { margin-top:3px; }
.select2-results__option .s2row__code { margin-top:1px; }
/* In the closed control the row is one clipped line, and the code chip is the
   part that survives the clipping: "CP-017 Kitchen Ma…" still says which point. */
/* The row is a block inside a box whose only vertical sizing is a line-height,
   and a block child ignores line-height: it sat on the top edge of the control
   with 15px of nothing under it. It takes the control's inner height itself
   and centres its pieces. */
.select2-selection__rendered .s2row {
  flex-wrap:nowrap; overflow:hidden; height:calc(var(--control-h) - 2px); align-items:center;
}
.select2-selection__rendered .s2row__text { display:block; min-width:0; }
.select2-selection__rendered .s2row__main { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* ---- the panel ------------------------------------------------------------ */
.select2-dropdown {
  border:1px solid var(--line-strong); border-radius:var(--radius-sm);
  box-shadow:var(--shadow-lg); overflow:hidden; background:var(--surface); box-sizing:border-box;
  /* dropdownAutoWidth (app.js) writes the control's width inline as the
     panel's min-width. The panel then takes the width of its longest row up
     to the cap, so a list of point names opens readable rather than as a
     column the width of a 150px filter box. app.js slides it left if that
     would hang off the right of the screen. */
  width:max-content !important;
  max-width:min(460px, calc(100vw - 16px));
}
.select2-container--open .select2-dropdown { z-index:65; }
.select2-container--default .select2-search--dropdown { padding:6px; }
.select2-container--default .select2-search--dropdown .select2-search__field {
  border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:6px 9px;
  font-family:inherit; font-size:13.5px; color:var(--text);
}
/* Three classes, to out-specify Select2's own three-class 200px cap — which it
   did not, so every dropdown in the app was a third shorter than intended. */
.select2-container--default .select2-results > .select2-results__options {
  max-height:min(320px, 46dvh);
}
/* Select2 ships a blue (#5897fb) highlight that fights this app's green. The
   overrides below re-theme it — and order matters: the *selected* rule has to
   come first so the *highlighted* rule can win over it. With them the other way
   round, hovering the option you already had chosen showed no feedback at all,
   because "selected" repainted over "hovered". */
.select2-container--default .select2-results__option {
  /* --text, as the chosen value is drawn in the closed control: on --text-2 the
     list read a shade fainter than the thing it was a list of. */
  padding:9px 12px; font-size:13.5px; color:var(--text); line-height:1.4;
  /* Wrap, in full. This used to clamp rows at two lines with -webkit-box,
     but a clamp on a box whose child is a flex row does not clamp — it clips
     the box's height, and the third line showed through as a strip of cut-off
     glyphs under the second. Nothing in a list should need hiding: with the
     panel at its own width a row rarely needs a second line at all. */
  white-space:normal; overflow-wrap:anywhere;
}
/* The chosen row is .select2-results__option--selected. It used to be keyed on
   [aria-selected=true], but Select2 4.1 also writes aria-selected=true on the
   row under the pointer, so every hovered row went bold and its text jumped
   half a character wider as the mouse passed over the list. */
.select2-container--default .select2-results__option--selected {
  background:var(--brand-50); color:var(--brand-800); font-weight:600;
}
/* Three classes: the vendor paints its blue on
   .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable,
   and anything shorter loses to it. */
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable,
.select2-container--default .select2-results__option--highlighted.select2-results__option--selected {
  background:var(--brand-600); color:#fff;
}
/* Children carry their own colour, so they need telling too — and the same
   ordering trap applies one level down. Selected first, highlighted wins. */
.select2-container--default .select2-results__option--selected .s2row__main { color:var(--brand-800); }
.select2-container--default .select2-results__option--highlighted .s2row__main,
.select2-container--default .select2-results__option--highlighted .s2row__sub { color:#fff; }
.select2-container--default .select2-results__option--highlighted .s2row__sub { opacity:.82; }
.select2-container--default .select2-results__option--highlighted .s2row__code {
  background:rgba(255,255,255,.18); border-color:transparent; color:#fff;
}
.select2-container--default .select2-results__option--selectable:hover { cursor:pointer; }
.select2-container--default .select2-results__option[role=group] { padding:0; }
.select2-container--default .select2-results__group {
  padding:8px 12px 5px; font-size:11px; font-weight:700; text-transform:uppercase;
  letter-spacing:.04em; color:var(--text-3); background:var(--surface-2);
}
.select2-container--default .select2-results__message { color:var(--text-3); }

/* ---- field-app profile ---------------------------------------------------
   The same widget with a control and rows a gloved thumb can hit. The control
   takes its height, line-height, chevron and clear button from --control-h, so
   one variable makes the whole control 50px; the rest is type size.

   The public complaint page shares it: a citizen standing at a bin is holding
   the same phone as the crew member, and 16px on the search box is what stops
   iOS zooming the page when it is focused. */
.fieldapp .select2-container--default,
.report .select2-container--default { --control-h:52px; }
.fieldapp .select2-container--default .select2-selection--single .select2-selection__rendered,
.report .select2-container--default .select2-selection--single .select2-selection__rendered {
  font-size:16px; padding-left:13px; padding-right:38px;
}
.fieldapp .select2-container--default .select2-selection--single .select2-selection__clear,
.report .select2-container--default .select2-selection--single .select2-selection__clear { margin-right:28px; }
/* Rounded like the boxes it sits between, not like a filter field. */
.report .select2-container--default .select2-selection--single { border-radius:10px; }
.select2-dropdown.select2--field .select2-results__option {
  padding:13px 14px; font-size:15px; min-height:52px; display:flex; align-items:center;
}
.select2--field .s2row__sub { font-size:12.5px; }
.select2-container--default .select2-dropdown.select2--field .select2-results > .select2-results__options {
  max-height:min(60dvh, 420px);
}
.select2-dropdown.select2--field .select2-search--dropdown .select2-search__field {
  padding:11px 12px; font-size:16px; /* 16px stops iOS zooming the page on focus */
}

/* ---- bottom sheet ---------------------------------------------------------
   On a phone a panel anchored to its control ends up half under the keyboard
   or off the top of the screen, and is only ever as wide as the control. So on
   any screen up to 720px — the field app, and the office pages opened on a
   handset — app.js lifts the list out to a sheet that rises from the bottom
   edge: it always opens in the same place, spans the screen, and the rows sit
   under the thumb. The scrim makes it obvious the rest of the page is waiting. */
@media (max-width:720px) {
  .select2-container--open.select2--sheet {
    position:fixed !important;
    /* Sits on the keyboard, not behind it. --kb-inset is 0 until app.js sees
       the keyboard open, so with no JS this is the old bottom:0. */
    top:auto !important; bottom:var(--kb-inset, 0px) !important;
    left:0 !important; right:0 !important;
    width:100% !important; max-width:640px; margin:0 auto; z-index:70;
  }
  /* Select2 positions the panel absolutely inside this wrapper, which leaves the
     wrapper zero-height. Pinned with bottom:0 that put an empty box on the
     viewport floor and the list flowed off the bottom of the screen — the whole
     sheet was invisible. Putting the panel back in flow gives the wrapper the
     panel's height, so bottom:0 lifts the list onto the screen. */
  .select2-container--open.select2--sheet > .select2-dropdown {
    position:static !important; top:auto !important; left:auto !important;
    width:100% !important; min-width:0 !important; max-width:none !important;
    border:0; border-radius:16px 16px 0 0; box-shadow:0 -10px 34px rgba(15,23,42,.28);
    padding:10px 0 calc(6px + env(safe-area-inset-bottom, 0px));
    animation:sheet-up .2s ease-out;
  }
  /* grab handle, so the panel reads as a sheet rather than a stuck menu */
  .select2-dropdown--sheet::before {
    content:''; display:block; width:38px; height:4px; border-radius:999px;
    background:var(--line-strong); margin:0 auto 10px;
  }
  .select2-dropdown--sheet .select2-search--dropdown { padding:0 14px 10px; }
  .select2-dropdown--sheet .select2-search--dropdown .select2-search__field {
    padding:11px 12px; font-size:16px; /* 16px stops iOS zooming the page on focus */
  }
  /* Rows on a sheet are thumb rows, whichever app opened it. */
  .select2-dropdown--sheet .select2-results__option {
    padding:13px 16px; font-size:15px; min-height:50px; display:flex; align-items:center;
  }
  .select2-dropdown--sheet .select2-results__group { padding:10px 16px 6px; }
  .select2-dropdown--sheet .s2row__sub { font-size:12.5px; }
  /* Against the space actually visible, so an open keyboard shortens the list
     instead of hiding it. Falls back to 52vh where visualViewport is missing.
     Three classes plus the modifier, to beat both the vendor cap and the
     field-profile cap above. */
  .select2-container--default .select2-dropdown--sheet .select2-results > .select2-results__options {
    max-height:52vh; max-height:min(calc(var(--vv-h, 100vh) * .52), 52dvh);
  }
}
@keyframes sheet-up { from { transform:translateY(14px); opacity:.4 } to { transform:none; opacity:1 } }
@media (prefers-reduced-motion:reduce) { .select2-dropdown--sheet { animation:none !important; } }

/* Dims the page behind an open sheet. Tapping it counts as an outside click,
   which is what Select2 already listens for, so it closes on its own. */
.sheet-scrim {
  position:fixed; inset:0; z-index:69; background:rgba(15,23,42,.45);
  animation:fade-in .18s ease-out;
}
@keyframes fade-in { from { opacity:0 } to { opacity:1 } }
@media (prefers-reduced-motion:reduce) { .sheet-scrim { animation:none; } }

/* A list of guarantees, not bullet points: the tick carries the sense and the
   text keeps one left edge when a line wraps, which a padded <ul> does not.
   The marker is positioned rather than a grid column, so an item holding markup
   — "<strong>Viewer</strong> — read-only access" — stays one paragraph instead
   of having each of its children promoted to a grid item of its own. */
.ticks { list-style:none; margin:0; padding:0; display:grid; gap:9px;
  font-size:13px; color:var(--text-2); line-height:1.55; }
.ticks li { position:relative; padding-left:23px; }
.ticks li::before {
  content:'\2713'; position:absolute; left:0; top:0; width:16px; text-align:center;
  color:var(--ok); font-weight:700; font-size:12px; line-height:1.55;
}
/* Same shape where the items are descriptions rather than assurances — a tick
   against "Viewer — read-only access" would be claiming something it isn't. */
.ticks--plain li::before { content:'\2022'; color:var(--brand-500); font-size:15px; }

/* fit-content(40%), not auto: an `auto` label column takes its longest label
   in full, and a long Bengali label starved the value beside it. */
.dl { display:grid; grid-template-columns:fit-content(40%) minmax(0,1fr); gap:8px 18px; font-size:13px; margin:0; }
.dl dt { color:var(--text-3); font-weight:500; }
.dl dd { margin:0; font-weight:550; }

.bar { height:9px; border-radius:5px; background:var(--muted-soft); overflow:hidden; }
.bar__fill { height:100%; border-radius:5px; background:var(--brand-500); }
/* Palette bars: `<div class="bar__fill bar__fill--c3">`. A ranked list reads far
   faster when each row carries its own colour than when it is eight greens. */
.bar__fill--c1{background:var(--c1)} .bar__fill--c2{background:var(--c2)}
.bar__fill--c3{background:var(--c3)} .bar__fill--c4{background:var(--c4)}
.bar__fill--c5{background:var(--c5)} .bar__fill--c6{background:var(--c6)}
.bar__fill--c7{background:var(--c7)} .bar__fill--c8{background:var(--c8)}

.stat-row { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--line); }
.stat-row:last-child { border-bottom:0; }
.stat-row__label { flex:1; font-size:13px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.stat-row__value { font-variant-numeric:tabular-nums; font-weight:650; font-size:13px; }
/* An inline <span> ignores width and height, so outside the flex rows it was
   first written for — a table cell, a modal heading — the swatch was drawing
   nothing at all. Blockified as a flex item either way, so the legends that
   already used it are unaffected. */
.swatch { display:inline-block; vertical-align:middle; width:9px; height:9px;
  border-radius:3px; flex:0 0 9px; box-shadow:inset 0 0 0 1px rgba(15,23,42,.12); }

/* Chart.js runs with maintainAspectRatio:false, so the canvas fills this box.
   The height is a floor rather than a fixed value: the box grows with the card
   so charts sharing a grid row line up top and bottom. */
.chart-box { position:relative; flex:1 1 auto; min-height:300px; }
.chart-box--sm { min-height:250px; }
/* A chart with a legend listed underneath it inside the same card. It keeps a
   fixed height rather than absorbing the card's slack, because the card is as
   tall as this box plus the legend — and whatever it grows to, the chart beside
   it in the grid row has to match. Pinning it keeps the pair compact. */
.chart-box--legend { flex:0 0 auto; height:255px; min-height:0; }
/* `isolation` is the whole point of this rule. Leaflet stacks its own furniture
   high inside the map — panes at 200-700, the zoom buttons and attribution at
   800-1000 — and none of that is measured against the page: a Select2 panel
   floats on <body> at z-index 65 (below the sidebar drawer, deliberately), so
   wherever a dropdown reached a map the pin and the zoom buttons punched
   straight through it and the list looked half-eaten. Isolating the map makes
   every number inside it local, so anything the page floats over it — a
   dropdown, the picking banner, a modal — simply wins. */
.map-box { height:520px; border-radius:var(--radius); overflow:hidden; border:1px solid var(--line);
  isolation:isolate; }
.map-box--sm { height:300px; }

/* ------------------------------------------------------------------ tracking
   The live map's fleet panel. It was a scrolling stack of tiles that only ever
   linked away: nothing on it moved the map, so picking one vehicle out of forty
   meant reading every permanent label on the tiles and hoping. Rows are buttons
   now — one selects, centres and follows — and the list searches, filters and
   pages so it stays the same size as the fleet grows. */
/* The map and the fleet panel are one screen-high row rather than two cards of
   guessed height. The old pair pinned the map at 520px and capped the panel at
   640px: two numbers that only agreed by accident, so the map card was stretched
   to the panel's cap and carried 30px of dead space under the tiles, and on a
   1366x768 laptop the row ran past the fold while a 1080p screen left a third of
   its height empty. One height on the grid row drives both — the map fills
   whatever the row is, the panel divides it — and the clamp keeps the whole page
   within the viewport from a small laptop up, which is what a page somebody
   leaves open on a wall wants. Below the sidebar breakpoint the two stack and the
   row height goes away, because there is nothing left to line up with. */
@media (min-width:1101px) {
  .grid--track { block-size:clamp(420px, calc(100vh - 250px), 780px); }
  .grid--track > .card { min-height:0; }
  .grid--track .card__body--flush { min-height:0; }
  .grid--track .map-box { height:100%; }
}
/* A tall map is the wrong half of a handset to spend on: at 520px it took two
   thirds of the screen and the list under it had to be scrolled to at all. */
@media (max-width:1100px) {
  .grid--track .map-box { height:min(58vh, 460px); }
}

.fleet { display:flex; flex-direction:column; min-height:0; }
/* The card's own furniture keeps its height; the list absorbs the slack. Without
   this the head was a flex item like any other and the browser squeezed it —
   13px of padding down to 3px, the "Vehicles" heading crushed against the border
   — while the rest of the panel still spilled out of the bottom of the card. */
.fleet > .card__head, .fleet__tools, .fleet__pager { flex:0 0 auto; }
.fleet__tools { padding:10px 14px; border-bottom:1px solid var(--line); display:flex;
  flex-direction:column; gap:8px; background:var(--surface-2); }
.fleet__tools-row { display:flex; gap:8px; }
.fleet__tools-row > select { flex:1 1 0; min-width:0; }
.fleet__search { width:100%; }

/* The list yields before the focus panel does. Both can shrink, but flexbox
   weights shrinking by content height, so an equal factor squeezed a 390px
   placement form into a 110px slit while the list kept 200px of rows nobody was
   reading at that moment. A heavier factor spends the list down to its floor
   first — for practical purposes entirely — and only then does the panel above
   it start to scroll. */
.fleet__list { flex:1 999 auto; overflow-y:auto; max-height:430px; min-height:110px; }
@media (min-width:1101px) { .fleet__list { max-height:none; } }
.fleet__row {
  display:flex; gap:10px; align-items:flex-start; width:100%; text-align:start;
  padding:10px 14px; border:0; border-bottom:1px solid var(--line); border-inline-start:3px solid transparent;
  background:none; font:inherit; color:inherit; cursor:pointer;
}
.fleet__row:hover { background:var(--surface-2); }
.fleet__row:focus-visible { outline:2px solid var(--brand-600); outline-offset:-2px; }
/* The selected row and the enlarged marker are the same state seen twice; the
   accent edge is what ties the two halves of the page together. */
.fleet__row.is-selected { background:var(--brand-50); border-inline-start-color:var(--brand-600); }
.fleet__row-dot { width:9px; height:9px; border-radius:50%; flex:0 0 9px; margin-top:5px;
  box-shadow:inset 0 0 0 1px rgba(15,23,42,.15); }
.fleet__row-body { display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 auto; }
.fleet__row-top { display:flex; justify-content:space-between; gap:8px; align-items:center; }
.fleet__row-reg { font-weight:650; font-size:13px; }
.fleet__row-sub { font-size:11.5px; color:var(--text-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fleet__row-signal { font-size:11px; color:var(--text-3); }
.fleet__signal--stale { color:var(--warn); font-weight:600; }
.fleet__signal--none { color:var(--text-3); font-style:italic; }
.fleet__empty { padding:26px 16px; text-align:center; font-size:12.5px; color:var(--text-3); }

.fleet__pager { display:flex; align-items:center; justify-content:space-between; gap:8px;
  flex-wrap:wrap; padding:8px 14px; border-top:1px solid var(--line);
  background:var(--surface-2); font-size:11.5px; color:var(--text-2); }
.fleet__pager-nav { display:flex; align-items:center; gap:6px; }
/* "1 / 2" is one token; without this it wrapped onto three lines in the narrow
   pager and read as three separate numbers. */
.fleet__pager-nav > span, .fleet__pager-info { white-space:nowrap; }
.fleet__pager-nav select { height:28px; padding:0 6px; font-size:11.5px; }
.fleet__pager button[disabled] { opacity:.4; cursor:default; }

/* An open placement form is taller than the space the panel has to give it, and
   it used to take that space anyway — out through the bottom of the card, over
   whatever the page drew next. It scrolls inside its own band instead, and the
   list keeps its floor, so neither half of the panel can push the other off. */
.fleet__focus { padding:11px 14px; border-bottom:1px solid var(--line);
  flex:0 1 auto; min-height:0; overflow-y:auto;
  background:color-mix(in srgb, var(--brand-600) 6%, var(--surface)); }
.fleet__focus-head { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
.fleet__focus-reg { font-weight:700; font-size:14px; }
.fleet__focus-meta { font-size:11.5px; color:var(--text-2); margin-top:2px; }
.fleet__focus-clear { border:0; background:none; font-size:20px; line-height:1; cursor:pointer;
  color:var(--text-3); padding:0 2px; }
.fleet__focus-clear:hover { color:var(--text); }
.fleet__focus-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:8px; }
.fleet__follow { font-size:12px; margin-inline-end:auto; }

.fleet__place { margin-top:9px; font-size:12px; }
.fleet__place > summary { cursor:pointer; font-weight:600; color:var(--brand-700); }
.fleet__place-form { margin-top:8px; display:flex; flex-direction:column; gap:8px; }
.fleet__place-row { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.fleet__place-actions { display:flex; gap:6px; flex-wrap:wrap; }

/* "Updated just now" over a map that has not moved since the network went away
   is worse than saying nothing, so the footer changes colour when a poll fails. */
.fleet__foot .is-stale { color:var(--bad); font-weight:600; }

/* Picking a coordinate off the map. The banner sits over the map rather than
   above it so the map does not jump when the mode is armed. */
.map-pick { position:absolute; z-index:500; inset-block-start:10px; inset-inline:10px;
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:7px 10px; border-radius:var(--radius); font-size:12px; font-weight:600;
  background:var(--surface); border:1px solid var(--brand-600);
  box-shadow:0 6px 18px rgba(15,23,42,.14); }
.card__body--flush:has(> .map-pick) { position:relative; }
.map-box.is-picking { cursor:crosshair; }
.map-box.is-picking .leaflet-container { cursor:crosshair; }

/* A vehicle's registration, drawn permanently beside its dot. */
.veh-label { background:rgba(15,23,42,.85); color:#fff; border:0; font-size:10.5px;
  font-weight:600; padding:1px 5px; border-radius:4px; box-shadow:none; }
.veh-label::before { display:none; }
/* A stale fix in the legend: the same washed-out, dashed dot the map draws. */
.swatch--stale { background:#0f8a74; opacity:.35; box-shadow:inset 0 0 0 1px rgba(15,23,42,.35); }

@media (max-width:900px) {
  .fleet__list { max-height:340px; }
}

.tabs { display:flex; gap:4px; border-bottom:1px solid var(--line); margin-bottom:16px; flex-wrap:wrap; }
.tabs a {
  /* inline-flex so a tab carrying an icon or a count badge keeps them on the
     same line as its label — as a plain block the icon sat above the words. */
  display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  padding:8px 14px; font-size:13px; font-weight:600; color:var(--text-2);
  border-bottom:2px solid transparent; margin-bottom:-1px;
}
.tabs a:hover { color:var(--brand-700); text-decoration:none; }
.tabs a.is-active { color:var(--brand-700); border-bottom-color:var(--brand-600); }

.legend { display:flex; flex-wrap:wrap; gap:8px 14px; font-size:12px; color:var(--text-2); }
.legend span { display:inline-flex; align-items:center; gap:6px; }

.item-row { display:grid; grid-template-columns:2fr 1fr 1.4fr 1.6fr auto; gap:8px; align-items:end; margin-bottom:8px; }
@media (max-width:820px){ .item-row { grid-template-columns:1fr 1fr; } }
.item-row > * { min-width:0; }

/* The stream rows carry their headings twice: as a header row above the table on
   a wide screen, and as a label inside every field on a narrow one. Only one is
   ever shown. Below 820px .item-row collapses to two columns, which wrapped the
   header row into a 2x2 block of its own — "Destination" and "Receiving actor"
   ended up directly above the waste-type and quantity boxes, naming the wrong
   controls. So the header row goes away there and each field says what it is. */
.item-row--head {
  font-size:11px; text-transform:uppercase; letter-spacing:.05em;
  color:var(--text-3); font-weight:650;
}
.item-row__label { display:none; }
@media (max-width:820px){
  .item-row--head { display:none; }
  .item-row { grid-template-columns:1fr; margin-bottom:14px; }
  .item-row__label { display:block; }
  .item-row + .item-row { border-top:1px solid var(--line); padding-top:12px; }
}

/* Label, control and hint on one line: a single exceptional field in a card
   foot, not a column in a form grid. */
.field--inline { flex-direction:row; align-items:center; gap:10px; flex-wrap:wrap; }
.field--inline input { width:140px; }
.field--inline .hint { flex:1 1 200px; }
/* A note that occupies a grid cell but is not a field. */
.field--note { margin-top:-6px; }

/* The week scrolls inside its own box; the page never scrolls sideways. */
/* ---------------- week calendar ----------------
   A row of days, opening on today. The week used to be seven columns of every
   individual pickup and nothing else — no way to ask about one day, and on a
   handset fifteen thousand pixels of near-identical tiles. */
.daybar {
  display:flex; gap:6px; overflow-x:auto; padding:0 2px 10px; margin:0 -2px 14px;
  scrollbar-width:thin;
}
.daybar__day {
  flex:1 0 auto; min-width:74px; display:flex; flex-direction:column; align-items:center; gap:1px;
  padding:8px 10px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); font:inherit; color:var(--text-2); cursor:pointer;
  transition:background .12s, border-color .12s;
}
.daybar__day:hover { border-color:var(--brand-500); }
.daybar__day[aria-selected="true"] {
  background:var(--brand-600); border-color:var(--brand-600); color:#fff;
}
.daybar__name { font-size:12px; font-weight:650; }
.daybar__count { font-size:16px; font-weight:700; font-variant-numeric:tabular-nums; }
.daybar__day[aria-selected="true"] .daybar__count { color:#fff; }
.daybar__today { font-size:9.5px; text-transform:uppercase; letter-spacing:.06em; font-weight:700;
  color:var(--brand-700); }
.daybar__day[aria-selected="true"] .daybar__today { color:var(--brand-100); }
/* The week, as a chip at the end of the days rather than a separate control. */
.daybar__day--week { border-style:dashed; }

/* The departure time is the first thing read on a run, so it leads the row. */
.run__time { display:block; font-size:15px; font-weight:700; font-variant-numeric:tabular-nums;
  color:var(--brand-700); line-height:1.2; }
.run__stops { flex:1 1 100%; font-size:12px; }
.run__stops > summary { cursor:pointer; color:var(--brand-700); font-weight:600; width:max-content; }
.run__list { margin:7px 0 0; padding-inline-start:20px; display:flex; flex-direction:column; gap:4px; }
.run__list li { font-size:12.5px; }
.run__list span { color:var(--text-3); font-size:11px; margin-inline-start:5px; }

/* Printing takes the whole week whichever day is open on screen: the day chips
   and their panels are `.no-print` and the clone drops them, which would leave
   the week grid hidden by the attribute the tab switcher set. */
.printable #weekGrid[hidden] { display:block !important; }

.week-scroll { overflow-x:auto; overflow-y:visible; margin:0 -2px; padding:0 2px 4px; }
.week-grid { display:grid; grid-template-columns:repeat(7,minmax(140px,1fr)); gap:10px; min-width:0; }
.week-col { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; }
.week-col__head { padding:8px 10px; background:var(--brand-50); border-bottom:1px solid var(--line);
  font-size:12px; font-weight:650; color:var(--brand-800); display:flex; justify-content:space-between; gap:8px; }
.week-col__body { padding:8px; display:flex; flex-direction:column; gap:6px; max-height:520px; overflow-y:auto; }
.week-item { padding:7px 8px; border-radius:6px; background:var(--surface-2); border:1px solid var(--line); font-size:11.5px; overflow-wrap:anywhere; }
.week-item strong { display:block; font-size:12px; font-weight:620; }
/* After the rules it overrides, not before them. `.week-col__body` sets
   max-height:520px unconditionally below, and with this block above it the
   handset override never applied — a week column stayed capped and scrolled
   inside itself on the one screen size where it should just run down the page. */
@media (max-width:820px) {
  /* A week read downwards beats a week dragged sideways on a handset. */
  .week-scroll { overflow-x:visible; }
  .week-grid { grid-template-columns:1fr; }
  .week-col__body { max-height:none; }
}

/* ---------------- the messages page ----------------
   Four tabs, one card. Reading takes the width — the composer used to hold a
   third of it on every visit, including the visits where nobody was writing —
   and writing is the fourth tab. Pinned, the composer leaves the tab and docks
   to the corner, where it is reachable from the other three. */
.msg-page { display:grid; gap:16px; }
.msg-page__compose { max-width:720px; }
/* Hidden on the reading tabs unless it has been pinned, in which case the pin
   rules below place it and this one must not take it away again. */
.msg-page__compose.is-tucked:not(.card--pinned) { display:none; }
/* On its own tab the composer *is* the page. Pinning decides whether it also
   follows you to the other three — not where it sits here. Without this, the
   pin threw the card into the corner and left the New message tab empty, with
   its own tab still underlined above a blank page. */
.msg-page__compose:not(.is-tucked).card--pinned {
  position:static; inset:auto; width:auto; max-width:720px;
  max-height:none; box-shadow:var(--shadow); z-index:auto;
}
.msg-page__list.is-hidden { display:none; }

/* ---------------- a card pinned to the screen ----------------
   The same card, docked to the bottom corner instead of standing in its column:
   a composer you keep coming back to, still there after scrolling a long list.
   It is a card, not a bar — the fields are the ones it always had — so the only
   things that change are where it sits and that its body scrolls once it is
   taller than the space it has. */
.card--pinned {
  position:fixed; z-index:45; inset-block-end:16px; inset-inline-end:16px;
  width:min(460px,calc(100vw - 32px)); max-height:min(76vh,680px);
  display:flex; flex-direction:column; box-shadow:var(--shadow-lg);
}
.card--pinned .card__body { overflow-y:auto; }
.card__pin { color:var(--text-3); }
.card__pin.is-on { color:var(--brand-700); background:var(--brand-50); }
/* Under the topbar, and clear of the flash stack. On a phone it spans the width
   and stops well short of full height: a docked form that covers the list it
   was opened from is worse than one that is a scroll away. */
@media (max-width:560px) {
  .card--pinned { inset-inline:8px; width:auto; max-height:64vh; inset-block-end:8px; }
}

/* ---------------- messages ----------------
   A row is a conversation, not one note: who it is with, the latest line, how
   many are behind it, and — in the sent list — whether it was opened. The whole
   row used to be one <a>, which left nowhere to put an action, so a message
   could be read and never removed, marked or answered from the list. */
.msg-row { display:flex; align-items:flex-start; gap:6px; border-bottom:1px solid var(--line); }
.msg-row:hover { background:var(--brand-50); }
.msg-row--unread { background:var(--brand-50); }
.msg-row--unread .msg-row__subject { font-weight:700; }
/* The row whose conversation the panel is showing. A left edge rather than a
   fill, so it is legible over the unread tint without fighting it. */
.msg-row--open { background:var(--surface-2); box-shadow:inset 3px 0 0 var(--brand-600); }
.msg-row--open.msg-row--unread { background:var(--brand-50); }
.msg-row__link { display:flex; gap:12px; flex:1; min-width:0; padding:12px 6px 12px 16px; color:inherit; }
/* The row's number in the list, as every register here numbers its rows: same
   size, same colour, same tabular figures, counted from the pager's offset so
   it is the place in the list rather than the place on the page. */
.msg-row__serial {
  flex:0 0 auto; min-width:18px; padding-top:11px; text-align:right;
  font-size:11.5px; color:var(--text-3); font-variant-numeric:tabular-nums;
}
.avatar--group { background:var(--surface-2); border:1px solid var(--line); color:var(--text-2); }
.msg-row__top { display:flex; gap:10px; align-items:baseline; min-width:0; overflow:hidden; }
/* Whether it landed, and when — as one group at the end of the line, so the two
   times are not two bare "8 min ago"s sitting side by side with nothing saying
   which is which. */
.msg-row__meta { display:flex; align-items:baseline; gap:8px; margin-left:auto; white-space:nowrap; }
.msg-row__meta .msg-row__receipt::after { content:'·'; margin-inline-start:8px; color:var(--text-3); }
.msg-row__receipt.is-read { color:var(--ok); }
.msg-row__name { margin-right:auto; }
/* Subject and the opening words on one line: a list is scanned as sentences,
   and four stacked lines per row turned twelve messages into a page of
   scrolling. The whole line truncates as one. */
.msg-row__line { display:flex; align-items:baseline; gap:8px; min-width:0; overflow:hidden; white-space:nowrap; }
.msg-row__line .msg-row__subject { flex:0 0 auto; max-width:52%; }
.msg-row__line .msg-row__preview { flex:1 1 auto; }
.msg-row__name { font-size:13px; font-weight:600; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.msg-row__role { font-weight:400; color:var(--text-3); font-size:11.5px; margin-left:4px; }
.msg-row__when { font-size:11.5px; color:var(--text-3); white-space:nowrap; }
.msg-row__subject { font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* How many notes are in the conversation, in the subject line where the count
   belongs to the thing it counts. */
.msg-row__count {
  display:inline-block; min-width:18px; padding:0 5px; margin-left:6px; border-radius:9px;
  background:var(--surface-2); border:1px solid var(--line); color:var(--text-2);
  font-size:11px; font-weight:650; text-align:center; font-variant-numeric:tabular-nums;
}
.msg-row__preview { color:var(--text-2); font-size:12.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.msg-row__receipt { font-size:11.5px; color:var(--text-3); white-space:nowrap; }
/* The actions stay out of the link — a <form> inside an <a> is not honoured by
   any browser — and out of the way until the row is hovered or focused within,
   so a list of forty does not read as a wall of buttons. */
.msg-row__actions { display:flex; align-items:center; gap:4px; padding:12px 12px 12px 0; opacity:0; transition:opacity .12s; }
.msg-row:hover .msg-row__actions, .msg-row:focus-within .msg-row__actions { opacity:1; }
@media (hover:none) { .msg-row__actions { opacity:1; } }
/* On a phone the row has to give the words the width. The role chip goes, the
   actions drop to their own line at the end of the row rather than eating into
   the name, and the subject is allowed two lines instead of being cut at
   "Md. N…" — which identifies nobody. */
@media (max-width:560px) {
  .msg-row { flex-wrap:wrap; }
  .msg-row__link { padding:12px 16px 8px; gap:10px; }
  .msg-row__serial { padding-top:10px; min-width:14px; }
  .msg-row__role { display:none; }
  .msg-row__actions { flex-basis:100%; justify-content:flex-end; padding:0 12px 10px; opacity:1; }
  .msg-row__line {
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
    white-space:normal; overflow:hidden;
  }
  /* The children carry their own nowrap for the desktop row; inside a clamped
     box that turns two wrapped lines back into one cut-off line. */
  .msg-row__line .msg-row__subject,
  .msg-row__line .msg-row__preview { white-space:normal; max-width:none; overflow:visible; }
}

/* The conversation itself: their lines on the left, mine on the right, in the
   order they were written. align-content, because a grid row stretches to the
   space it is given: in a panel the height of the screen, one message became
   one balloon six hundred pixels tall. */
.thread { display:grid; gap:12px; align-content:start; }
/* In the reading pane the column is a third of the page, so the bubbles take
   nearly all of it and the pane scrolls rather than growing the page past the
   list beside it. */
.thread--pane { max-height:min(46vh,420px); overflow-y:auto; padding-right:2px;
  display:grid; gap:12px; }
.thread--pane .bubble { max-width:92%; }
.thread--pane .bubble__body { font-size:13.5px; }
.bubble {
  max-width:min(78%,620px); padding:10px 13px; border-radius:var(--radius);
  background:var(--surface-2); border:1px solid var(--line);
}
.bubble--mine { margin-inline-start:auto; background:var(--brand-50);
  border-color:color-mix(in srgb, var(--brand-600) 28%, var(--line)); }
.bubble__head { display:flex; gap:10px; align-items:baseline; margin-bottom:4px; }
.bubble__who { font-size:12.5px; font-weight:650; }
.bubble__meta { font-size:11px; color:var(--text-3); margin-left:auto; white-space:nowrap; }
.bubble__body { font-size:14px; line-height:1.65; white-space:pre-wrap; overflow-wrap:anywhere; }
.bubble__receipt { margin-top:6px; font-size:11px; color:var(--text-3); text-align:right; }
/* A destructive action that is not the page's main act: the word is red, the
   button is not. Deleting a conversation sits in a row of header buttons and a
   solid red block there reads as the thing to do. */
.btn--danger-text { color:var(--bad); }
.btn--danger-text:hover { background:var(--bad-soft); border-color:var(--bad); color:var(--bad); }

.thread__reply { display:block; }
.thread__reply textarea { min-height:84px; }
@media (max-width:560px) { .bubble { max-width:100%; } }

/* ---------------- notification tray ----------------
   A panel, not a dropdown list. The old one was eight identical rows with the
   time at the bottom of each: nothing said which notice mattered, nothing said
   when "3 d ago" stopped and "4 d ago" started, and a long body pushed the rest
   of the tray off the screen. */
.notif-tray {
  position:absolute; right:16px; top:52px; width:376px; max-width:calc(100vw - 24px);
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow-lg); z-index:60; overflow:hidden;
  display:flex; flex-direction:column; max-height:min(30rem, calc(100vh - 76px));
  transform-origin:top right; animation:notif-pop .13s ease-out;
}
@keyframes notif-pop { from { opacity:0; transform:translateY(-6px) scale(.985); } }
@media (prefers-reduced-motion:reduce) { .notif-tray { animation:none; } }

.notif-tray__head {
  display:flex; align-items:center; gap:9px; padding:11px 14px; flex:0 0 auto;
  border-bottom:1px solid var(--line); background:var(--surface-2);
}
.notif-tray__head h3 { margin:0; font-size:13.5px; font-weight:650; }
/* The unread figure next to the title rather than only on the bell: the bell is
   behind the tray once the tray is open. */
.notif-tray__count {
  font-size:11px; font-weight:650; padding:2px 7px; border-radius:999px;
  background:var(--brand-100); color:var(--brand-800);
}
.notif-tray__readall { margin-left:auto; }

/* Only this scrolls, so the header and the day headings stay put. */
.notif-tray__list { overflow-y:auto; overscroll-behavior:contain; flex:1 1 auto; }

/* Sticky, because the heading a row belongs under is worth more when you have
   scrolled away from it than when you can already see it. */
.notif-group {
  position:sticky; top:0; z-index:1; padding:6px 14px;
  background:color-mix(in srgb, var(--surface-2) 92%, transparent);
  backdrop-filter:blur(6px);
  border-bottom:1px solid var(--line);
  font-size:10.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  color:var(--text-3);
}

.notif {
  --notif-tone:var(--info);
  display:flex; gap:10px; align-items:flex-start;
  padding:10px 14px 10px 12px; border-bottom:1px solid var(--line);
  border-left:3px solid transparent; font-size:12.5px; color:var(--text);
}
a.notif:hover { background:var(--surface-2); text-decoration:none; }
.notif--ok   { --notif-tone:var(--ok); }
.notif--warn { --notif-tone:var(--warn); }
.notif--bad  { --notif-tone:var(--bad); }
.notif--info { --notif-tone:var(--info); }
/* The tone is carried by the icon always, and by the left edge only while the
   notice is unread — so the accent marks what still needs attention rather than
   striping the whole list. */
.notif--unread { border-left-color:var(--notif-tone); background:var(--brand-50); }
a.notif--unread:hover { background:color-mix(in srgb, var(--brand-50) 65%, var(--surface-2)); }

.notif__icon {
  flex:0 0 auto; display:grid; place-items:center; width:26px; height:26px;
  border-radius:50%; margin-top:1px;
  color:var(--notif-tone); background:color-mix(in srgb, var(--notif-tone) 12%, var(--surface));
}
.notif__main { min-width:0; flex:1 1 auto; }
.notif__title { font-weight:650; font-size:13px; line-height:1.35; }
/* Two lines and then an ellipsis. One notification used to be able to make the
   tray taller than the window; the body is a preview, and the link is the way
   to the whole of it. */
.notif__body {
  color:var(--text-2); font-size:12px; line-height:1.45; margin-top:2px;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.notif__foot { display:flex; align-items:center; gap:7px; margin-top:4px; }
.notif__time { color:var(--text-3); font-size:11px; }
.notif__new {
  font-size:9.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  padding:1px 5px; border-radius:999px;
  color:var(--notif-tone); background:color-mix(in srgb, var(--notif-tone) 14%, var(--surface));
}

.notif-empty { padding:30px 20px; text-align:center; }
.notif-empty__icon {
  display:inline-grid; place-items:center; width:42px; height:42px; border-radius:50%;
  background:var(--surface-2); color:var(--text-3); margin-bottom:9px;
}
.notif-empty__title { font-size:13px; font-weight:600; }
.notif-empty__hint { font-size:11.5px; color:var(--text-3); margin-top:3px; }

/* The way out of the tray and into the archive. Sticks to the bottom so it is
   reachable without scrolling the list to its end. */
.notif-tray__all {
  flex:0 0 auto; display:block; padding:9px 14px; text-align:center;
  border-top:1px solid var(--line); background:var(--surface-2);
  font-size:12px; font-weight:650; color:var(--brand-700);
}
.notif-tray__all:hover { background:var(--brand-50); text-decoration:none; }

@media (max-width:560px) {
  .notif-tray { right:8px; left:8px; width:auto; }
}

/* ---------------- notifications page ----------------
   The same row as the tray, given room: the whole timestamp rather than "3 h
   ago", and the per-item actions the tray has no space for. */
.notif-list .notif { padding-right:8px; }
.notif-list .notif__title { font-size:13.5px; }
/* Only the text is the link. The actions sit outside it, so a click meant for
   "dismiss" can never be a click that opens the notice instead. */
.notif__link { display:block; color:inherit; min-width:0; }
.notif__link:hover { text-decoration:none; }
.notif__link:hover .notif__title { color:var(--brand-700); }
.notif__actions { flex:0 0 auto; display:flex; gap:2px; align-items:center; margin-top:1px; }
.notif__actions .btn { padding:0 7px; min-height:28px; color:var(--text-3); }
.notif__actions .btn:hover { color:var(--text); }
.notif-list .notif-group { top:0; }

.notif-detail { --notif-tone:var(--info); border-top:3px solid var(--notif-tone); }
.notif-detail--ok   { --notif-tone:var(--ok); }
.notif-detail--warn { --notif-tone:var(--warn); }
.notif-detail--bad  { --notif-tone:var(--bad); }
.notif-detail--info { --notif-tone:var(--info); }
.notif-detail__head { display:flex; gap:12px; align-items:flex-start; }
.notif-detail__icon {
  flex:0 0 auto; display:grid; place-items:center; width:38px; height:38px; border-radius:50%;
  color:var(--notif-tone); background:color-mix(in srgb, var(--notif-tone) 12%, var(--surface));
}
.notif-detail__title { margin:0; font-size:18px; line-height:1.3; }
.notif-detail__meta {
  display:flex; flex-wrap:wrap; gap:8px; align-items:center;
  margin-top:6px; font-size:12px; color:var(--text-2);
}
.notif-detail__ago { color:var(--text-3); }
/* pre-wrap, because the notifier templates write "Type: …\nScheduled: …" and
   those line breaks are a small table, not incidental whitespace. */
.notif-detail__body {
  margin-top:16px; padding:14px; border-radius:var(--radius-sm);
  background:var(--surface-2); border:1px solid var(--line);
  font-size:13.5px; line-height:1.6; white-space:pre-wrap; overflow-wrap:anywhere;
}
.notif-detail__actions {
  display:flex; flex-wrap:wrap; gap:8px; align-items:center;
  margin-top:18px; padding-top:14px; border-top:1px solid var(--line);
}

/* ---------------- auth ---------------- */
.auth {
  min-height:100vh; display:grid; grid-template-columns:1.05fr .95fr;
  background:var(--surface);
}
.auth__aside {
  background:linear-gradient(150deg,var(--brand-900),var(--brand-700) 60%,var(--brand-600));
  color:#eafaf6; padding:52px 48px; display:flex; flex-direction:column; justify-content:center;
  position:relative; overflow:hidden;
}
.auth__aside::after {
  content:""; position:absolute; inset:auto -80px -120px auto; width:340px; height:340px;
  border-radius:50%; background:rgba(255,255,255,.06);
}
.auth__aside h1 { font-size:30px; color:#fff; margin-bottom:12px; letter-spacing:-.02em; }
.auth__aside p { color:#b9ddd4; font-size:14.5px; max-width:430px; }
.auth__features { margin-top:26px; display:grid; gap:11px; }
.auth__feature { display:flex; gap:11px; align-items:flex-start; font-size:13.5px; color:#d6f0e9; }
.auth__feature span:first-child { color:var(--brand-500); font-size:15px; }
/* Moved below `.auth__aside`, which sets display:flex. Written above it, this
   hide never took effect and the decorative panel stayed on screen on a phone,
   taking half the height the sign-in form needed. */
@media (max-width:900px) {
  .auth { grid-template-columns:1fr; }
  .auth__aside { display:none; }
}
.auth__form { display:grid; place-items:center; padding:40px 24px; }
.auth__box { width:100%; max-width:380px; }
.auth__bar { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:14px; }
/* The door to the citizens' page: a real button, the height of Sign in, in
   the brand's outline so it reads as a second action and not as a footnote. */
.auth__home-btn {
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:9px 14px; font-size:13.5px; font-weight:700; border-radius:10px;
  background:var(--brand-50); color:var(--brand-800); border:1.5px solid var(--brand-500);
}
.auth__home-btn:hover { background:var(--brand-600); color:#fff; border-color:var(--brand-600); text-decoration:none; }
.auth__home-btn:focus-visible { outline:3px solid color-mix(in srgb, var(--brand-600) 40%, transparent); outline-offset:2px; }
.auth__demo {
  margin-top:22px; padding:13px 14px; background:var(--surface-2);
  border:1px dashed var(--line-strong); border-radius:var(--radius); font-size:12px; color:var(--text-2);
}
.auth__demo table { width:100%; font-size:11.5px; border-collapse:collapse; }
.auth__demo td { padding:0; }
.auth__demo-hint { font-size:11px; color:var(--text-3); margin-bottom:6px; }
/* Each demo account is a button, not a line of text to be retyped. */
.auth__demo-row {
  display:flex; align-items:center; gap:10px; width:100%; text-align:left;
  padding:7px 9px; margin:2px 0; cursor:pointer; font:inherit; font-size:11.5px;
  background:transparent; border:1px solid transparent; border-radius:var(--radius-sm);
  color:var(--text-2);
}
.auth__demo-row:hover { background:var(--surface); border-color:var(--line); }
.auth__demo-row.is-picked { background:var(--brand-50); border-color:var(--brand-500); color:var(--brand-800); }
.auth__demo-role { font-weight:650; color:var(--text); flex:0 0 auto; }
.auth__demo-cred { font-family:var(--mono); margin-left:auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.auth__demo-go { opacity:0; flex:0 0 auto; }
.auth__demo-row:hover .auth__demo-go, .auth__demo-row.is-picked .auth__demo-go { opacity:.7; }

.error-page { display:grid; place-items:center; min-height:70vh; text-align:center; }
.error-page__code { font-size:64px; font-weight:750; color:var(--brand-600); letter-spacing:-.03em; }

/* ---------------- inline notices ---------------- */
.notice {
  display:flex; gap:10px; align-items:flex-start;
  padding:11px 14px; border-radius:var(--radius); font-size:13px;
  border:1px solid var(--line); background:var(--surface-2); color:var(--text-2);
}
.notice strong { color:var(--text); }
.notice--warn { background:var(--warn-soft); border-color:#fcd34d; color:#92400e; }
.notice--warn strong { color:#78350f; }
.notice--bad  { background:var(--bad-soft);  border-color:#fca5a5; color:#991b1b; }
.notice--ok   { background:var(--ok-soft);   border-color:#86efac; color:#166534; }
.notice--info { background:var(--info-soft); border-color:#7dd3fc; color:#075985; }

/* ---------------- route stop list ---------------- */
/* ---------------- record list ----------------
   A register rendered as rows rather than as a table, for the screens whose
   records have a shape rather than a fixed set of columns — a round, a service
   gap, a declared day. The rounds list was a twelve-column table: at 1440px two
   columns were already hidden behind a scrollbar, and on a phone everything
   between the name and the Edit button was, which is every fact a supervisor
   opens the screen to read.

   One rendering, not two. Each row is a flex line that wraps: identity first,
   then its facts, then what you can do about it. At a desk that is a dense
   row; on a handset the same row becomes a small card, with nothing dropped
   and nothing to drag sideways. */
.recs { list-style:none; margin:0; padding:0; }
.recs__item {
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 18px;
  padding:13px 16px; border-bottom:1px solid var(--line);
}
.recs__item:last-child { border-bottom:0; }
.recs__item:hover { background:var(--brand-50); }
/* A row needing attention — a round with no vehicle, an absence with no cover. */
.recs__item--flag { border-inline-start:3px solid var(--warn); }
.recs__item--bad { border-inline-start:3px solid var(--bad); }

.recs__id { flex:1 1 250px; min-width:0; }
.recs__title { font-weight:650; font-size:13.5px; display:block; }
.recs__sub { font-size:11.5px; color:var(--text-2); margin-top:2px; }
.recs__tags { display:flex; flex-wrap:wrap; gap:5px; margin-top:6px; }

/* The facts a row is compared on. `dt` above `dd` so a label never has to be
   guessed from position the way a table cell under a scrolled-away heading does. */
.recs__facts { display:flex; flex-wrap:wrap; gap:6px 16px; margin:0; }
.recs__facts > div { min-width:52px; }
.recs__flag { font-size:10px; font-weight:700; color:var(--bad); text-transform:uppercase; letter-spacing:.03em; }
.recs__facts dt { font-size:10px; text-transform:uppercase; letter-spacing:.05em;
  color:var(--text-3); font-weight:650; }
.recs__facts dd { margin:0; font-size:13px; font-weight:600; font-variant-numeric:tabular-nums; }
.recs__facts dd small { font-weight:500; color:var(--text-3); font-size:11px; }
.recs__facts .is-bad dd { color:var(--bad); }
.recs__facts .is-warn dd { color:var(--warn); }

.recs__who { flex:0 1 150px; min-width:0; font-size:12.5px; }
.recs__who strong { display:block; font-weight:650; }
.recs__who span { color:var(--text-3); font-size:11.5px; }
/* Two facts about a row that are both names — a vehicle registration and the
   person leading the round. Stacked bare, as they were, the pair is only
   readable by somebody who already knows the fleet, and it is the reader who
   does not that needs it. A label column keeps the values aligned down the
   list, so the vans can still be compared by eye. */
.recs__who--pairs {
  display:grid; grid-template-columns:auto minmax(0,1fr); gap:3px 8px; align-items:baseline;
}
.recs__who--pairs dt {
  font-size:10px; text-transform:uppercase; letter-spacing:.05em;
  color:var(--text-3); font-weight:650;
}
.recs__who--pairs dd {
  margin:0; font-weight:650; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.recs__who--pairs dd.is-bad { color:var(--bad); }
.recs__who--pairs dd.is-empty { color:var(--text-3); font-weight:500; }
.recs__actions { margin-inline-start:auto; display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.recs__actions form { display:inline; }

/* Down a list, the same fact should sit at the same place in every row —
   otherwise the four figures cannot be compared by eye, which is most of what a
   list of rounds is read for. The identity block is the only thing that flexes,
   and every other part is a fixed width, so what is left over for it is the same
   on every row and the columns line up without the list being a table. */
@media (min-width:900px) {
  .recs__facts > div { flex:0 0 auto; min-width:80px; }
  .recs__who { flex:0 0 150px; }
  .recs__who--pairs { flex:0 0 195px; }
  .recs__actions { flex:0 0 auto; }
}

@media (max-width:640px) {
  /* Actions become a full-width row of their own rather than a squeezed corner:
     on a handset the thing you came to press should be the width of a thumb. */
  .recs__item { gap:9px 12px; padding:12px 14px; }
  .recs__id, .recs__who, .recs__facts, .recs__actions { flex:1 1 100%; margin-inline-start:0; }
  .recs__actions .btn { flex:1 1 auto; justify-content:center; }
  .recs__facts { gap:6px 14px; }
}

/* A small inline label — a shift, a set of operating days, a ward. Reads as a
   fact about the record rather than as a status, which the badge is for. */
.chip-soft {
  display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:999px;
  background:var(--surface-2); border:1px solid var(--line);
  font-size:11px; font-weight:600; color:var(--text-2); white-space:nowrap;
}
.chip-soft--warn { background:var(--warn-soft); border-color:transparent; color:#92400e; }
.chip-soft--bad { background:var(--bad-soft); border-color:transparent; color:#991b1b; }

.stops { list-style:none; margin:0; padding:0; }
.stops li {
  display:flex; gap:12px; align-items:center; padding:10px 14px;
  border-bottom:1px solid var(--line); font-size:13px;
}
.stops li:last-child { border-bottom:0; }
.stops__seq {
  width:26px; height:26px; flex:0 0 26px; border-radius:50%;
  background:var(--brand-600); color:#fff; display:grid; place-items:center;
  font-size:12px; font-weight:650;
}
.stops__body { flex:1; min-width:0; }
.stops__meta { font-size:11.5px; color:var(--text-3); }
.stops li.is-dragging { opacity:.45; }
.stops__grip { cursor:grab; color:var(--text-3); font-size:15px; user-select:none; }
/* Reordering without a mouse. The grip is a mouse affordance and HTML5 drag
   fires on nothing else, so these carry the same job for touch and keyboard. */
.stops__move { display:flex; flex-direction:column; gap:2px; flex:0 0 auto; }
.stops__mv {
  width:26px; height:19px; display:grid; place-items:center; padding:0;
  border:1px solid var(--line); border-radius:5px; background:var(--surface);
  color:var(--text-2); font-size:9px; line-height:1; cursor:pointer;
}
.stops__mv:hover:not([disabled]) { border-color:var(--brand-500); color:var(--brand-700); }
.stops__mv:focus-visible { outline:2px solid var(--brand-600); outline-offset:1px; }
.stops__mv[disabled] { opacity:.35; cursor:default; }
@media (max-width:640px) {
  /* A stop row carries five things and 390px fits three of them. So it wraps
     into two lines with the order set explicitly rather than by source: what
     identifies the stop and what you do to it stay on the first line, and the
     figures — which are read, not pressed — drop to the second, indented to the
     name they describe. Left to plain wrapping the remove button ended up on a
     line of its own and a single stop was 280px tall. */
  /* Scoped to .stops. Unscoped, `order:2` reached the field app's round sheet,
     which reuses .stops__seq inside a .field__stop row — and moved the stop's
     number past the name AND past the chevron, so a numbered list read
     "name › To do (1)". The rules below are about the desktop route sheet's own
     wrapping row and belong to it. */
  .stops li { flex-wrap:wrap; gap:6px 10px; padding:10px 12px; align-items:flex-start; }
  .stops .stops__move { order:1; }
  .stops .stops__seq { order:2; margin-top:2px; }
  .stops .stops__body { order:3; flex:1 1 55%; }
  .stops li > form { order:4; margin-inline-start:auto; }
  .stops li > div[style*="text-align:right"] {
    order:5; flex:1 1 100%; text-align:start !important;
    padding-inline-start:70px; font-size:11.5px;
  }
  /* Bigger targets where the pointer is a finger; the grip is a mouse gesture
     that a touch screen cannot perform, so it goes. */
  .stops__mv { width:34px; height:26px; font-size:10px; }
  .stops__grip { display:none; }
}

/* ---------------- picker (multi-select of points) ---------------- */
.picker { border:1px solid var(--line); border-radius:var(--radius-sm); max-height:320px; overflow:auto; }
.picker label {
  display:flex; gap:9px; align-items:center; padding:7px 11px;
  border-bottom:1px solid var(--line); font-size:13px; font-weight:400; cursor:pointer;
}
.picker label:last-child { border-bottom:0; }
.picker label:hover { background:var(--brand-50); }
.picker input { margin:0; }
.picker__meta { margin-left:auto; font-size:11.5px; color:var(--text-3); }

/* ---------------- documentation prose ---------------- */
.prose { max-width:820px; font-size:14px; line-height:1.65; color:var(--text-2); }
.prose h1 { font-size:22px; color:var(--text); margin:0 0 6px; }
.prose h2 { font-size:17px; color:var(--text); margin:30px 0 10px; padding-bottom:6px; border-bottom:1px solid var(--line); }
.prose h3 { font-size:14.5px; color:var(--text); margin:22px 0 8px; }
.prose p { margin:0 0 12px; }
.prose ul, .prose ol { margin:0 0 12px; padding-left:22px; }
.prose li { margin-bottom:5px; }
.prose table { width:100%; border-collapse:collapse; margin:0 0 16px; font-size:13px; }
/* A wide Markdown table (the data dictionary) must scroll in its own box; without
   this it widens the whole document and every page scrolls sideways. help.js wraps
   each rendered table in .table-wrap so the rule has something to bite on. */
.prose .table-wrap { overflow-x:auto; margin:0 0 16px; }
.prose .table-wrap > table { margin:0; width:auto; min-width:100%; }
.prose th, .prose td { border:1px solid var(--line); padding:7px 10px; text-align:left; vertical-align:top; }
.prose th { background:var(--surface-2); font-weight:600; color:var(--text); }
.prose blockquote {
  margin:0 0 14px; padding:10px 14px; border-left:3px solid var(--brand-500);
  background:var(--brand-50); color:var(--brand-800); border-radius:0 var(--radius-sm) var(--radius-sm) 0;
}
.prose pre {
  background:#0f172a; color:#e2e8f0; padding:13px 15px; border-radius:var(--radius-sm);
  overflow-x:auto; font-size:12.5px; line-height:1.5; margin:0 0 14px;
}
.prose pre code { background:none; border:0; color:inherit; padding:0; font-size:12.5px; }
/* Inline code in a help page is prose, and wraps like it. */
.prose :not(pre) > code { white-space:normal; overflow-wrap:anywhere; }
.prose hr { border:0; border-top:1px solid var(--line); margin:26px 0; }
/* Diagram sources are shown as source in-app — rendering them would mean
   vendoring a 3.5 MB library onto a page crews never open. They draw as
   diagrams in the delivered Markdown files and in any Markdown viewer. */
.prose pre:has(> code.language-mermaid) { position:relative; padding-top:30px; background:#111c2e; }
.prose pre > code.language-mermaid::before {
  content:'Diagram source — renders as a diagram in the Markdown file';
  position:absolute; top:0; left:0; right:0; padding:5px 15px;
  font-family:var(--font); font-size:11px; letter-spacing:.03em;
  color:#94a3b8; background:rgba(148,163,184,.12); border-bottom:1px solid rgba(148,163,184,.2);
}
.prose a { text-decoration:underline; }

/* Draws the eye to an unresolved staffing gap without shouting like a danger button. */
.btn--warn { background:var(--warn-soft); border-color:#fcd34d; color:#92400e; font-weight:650; }
.btn--warn:hover { background:#fde68a; border-color:#f59e0b; }

.field__note { font-size:12.5px; color:var(--text-3); line-height:1.5; margin:0 0 11px; }

/* Inline SVG icons sit on the text baseline and take the colour around them. */
.icon { display:block; flex:0 0 auto; }
/* Side-menu and topbar icon slots centre their SVG. */
.nav__icon, .sidebar__logo, .topbar__btn { display:grid; place-items:center; }
.topbar__menu { place-items:center; }
/* Collapsible nav group: the summary is a nav row, its chevron turns when open. */
/* A folding group heading must read as the same kind of thing as a plain one:
   same size, weight, colour, tracking and left edge. The <summary> only adds a
   chevron and a hit area — it does not get its own typography. */
.nav__group--fold > summary { list-style:none; cursor:pointer; }
.nav__group--fold > summary::-webkit-details-marker { display:none; }
.nav__label { display:flex; align-items:center; gap:8px; }
/* Bangla section headings are not upper-cased: the script has no case. */
.lang-bn .nav__sec { text-transform:none; font-size:11px; }
.nav__label-text { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.nav__label--fold { border-radius:7px; }
.nav__label--fold:hover { background:rgba(255,255,255,.07); color:#a8d5c9; }
.nav__label--fold:focus-visible { outline:2px solid var(--brand-500); outline-offset:1px; }
.nav__chev { margin-left:auto; display:grid; place-items:center; opacity:.6; transition:transform .16s ease; }
.nav__group--fold[open] > summary .nav__chev { transform:rotate(90deg); }
@media (prefers-reduced-motion:reduce) { .nav__chev { transition:none; } }
.nav__icon { width:20px; }
/* Items inside a folding group line up with items in a plain one. */
.nav__group--fold > .nav__item { margin-left:0; }
.empty__icon .icon { margin:0 auto; }
/* Icon + label buttons keep both on one baseline with a consistent gap. */
.btn, .field__big, .field__drawer-out { display:inline-flex; align-items:center; justify-content:center; gap:8px; }
.btn--block { width:100%; }
.card__icon .icon, .wiz__icon .icon, .kpi__icon .icon,
.msg-card__icon .icon, .choice__icon .icon, .field__bar-btn .icon,
.field__brand-mark .icon, .doc-card__icon .icon { margin:0; }

.photo-cell__remove { display:grid; place-items:center; }

.icon--inline { display:inline-block; vertical-align:-0.18em; }
button .icon, a .icon { pointer-events:none; }

/* ---------------- responsive safety net ----------------
   Audited across every page. These are the rules that stop a single long value
   — a point name, an email address, a device UUID, a wide table — from forcing
   the whole document to scroll sideways on a phone. */

/* Nothing may push the document wider than the viewport. `clip` rather than
   `hidden`, and on <body> only: overflow-x:hidden on <html> makes some mobile
   browsers treat the viewport as a scroll container, which breaks every
   position:fixed element — including the field app's bottom bar. */
html, body { max-width:100%; }
body { overflow-x:clip; }
@supports not (overflow:clip) { body { overflow-x:hidden; } }

/* Flex and grid children default to min-width:auto, which is "as wide as my
   content" — the usual cause of a row that will not shrink. */
.filters > *, .btn-group > *, .page-head > *, .card__head > *,
.grid > *, .form-grid > *, .stat-row > * { min-width:0; }

/* Two utilities for free text in a slot of fixed size, so a view does not
   have to spell the idiom out inline each time:
     .truncate   one line, clipped with an ellipsis
     .clamp-2    two lines, then clipped
   Both are for places where the whole text is also available somewhere — a
   title attribute, the record's own page. */
.truncate { display:inline-block; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom; }
.clamp-2 { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; white-space:normal; overflow-wrap:anywhere; }

/* Long unbroken strings: emails, URLs, device codes. `break-word` rather than
   `anywhere`, so a word is only split when it cannot fit on a line of its own —
   `anywhere` broke ordinary names mid-word the moment a column was narrow, which
   with the table min-widths above is no longer the way to make one fit. */
.tbl td, .dl dd, .msg-card__text, .notice div, .empty div, .sheet__table td { overflow-wrap:break-word; }
.page-head__sub, .notif, .card__sub { overflow-wrap:anywhere; }
.mono-sm { overflow-wrap:anywhere; }

/* A filter bar is a wrapping row on a phone, not a horizontal scroller. */
.filters { flex-wrap:wrap; }
@media (max-width:640px) {
  .filters > .field { flex:1 1 100%; min-width:0; }
  .filters > .field[style*="min-width"] { min-width:0 !important; }
  /* The search box on the point and actor registers carries `flex:1` inline, to
     take the slack on a wide bar. Inline beats the stylesheet, so on a phone it
     kept a basis of zero while the rule above took its floor away too — and the
     one field somebody actually types into closed to a 30px slot wedged against
     the next label. It is a full-width row here like every other field. */
  .filters > .field[style*="flex:1"] { flex:1 1 100% !important; }
  .filters .btn-group { width:100%; }
  .filters .btn-group .btn { flex:1; }
}

/* Page heading and its actions stack rather than squeeze. */
@media (max-width:640px) {
  .page-head { flex-direction:column; align-items:stretch; }
  .page-head__actions { width:100%; }
  .page-head__actions .btn { flex:1 1 auto; justify-content:center; }
  .card__head { flex-wrap:wrap; }
  .card__foot { flex-wrap:wrap; }
  .tabs { overflow-x:auto; }
}

/* Inline edit panels inside a table cell were pinned wider than a phone. */
@media (max-width:560px) {
  details form[style*="min-width"] { min-width:0 !important; width:100%; }
  .dl { grid-template-columns:1fr; }
}

/* Charts and maps keep their aspect without forcing width. */
.chart-box, .map-box { max-width:100%; }
canvas { max-width:100%; }

/* ---------------- printable report sheet ----------------
   Sized for A4. The browser's "Save as PDF" is the PDF generator here: every
   Node PDF library draws text glyph-by-glyph without complex-script shaping, so
   Bangla conjuncts and matras come out broken. The browser has a real text
   engine and the same webfont as the rest of the app, so it does not. */
.printable { background:#fff; }
/* The preview frame renders at screen width, so the paper's rules about what a
   register is *not* carrying have to hold there too — otherwise the sheet on
   screen is not the sheet that comes out. The clone drops the controls
   (app.js); this stops the cells they vacated from still claiming a column. */
.printable .tbl th:empty, .printable .tbl td:empty { padding:0; width:0; border:0; }
/* A register that scrolls sideways on screen has nowhere to scroll on a sheet
   of paper, and nowhere in the preview either: left at its screen width the
   last columns — the tonnage and the status — were simply off the edge of the
   frame. Same bargain the print block strikes below: cells wrap, and a
   registration number over two lines beats a column that is not there. The
   !important is for the :has() min-widths, which outrank this selector. */
/* The frame is A4 at 96dpi, so the paper's type size is the preview's type
   size — leaving the register at its screen 13px showed a sheet a column
   wider than the one that comes out of the printer. */
.printable .tbl { font-size:10.5px; }
/* The 14px gutters a register uses on screen are 28px per column: over nine
   columns that is a third of the sheet spent on air, and the last column was
   left sitting on the edge of the frame. */
.printable .tbl th, .printable .tbl td { padding-left:7px; padding-right:7px; }
/* And the same argument vertically. A register on screen is a comfortable
   10px per cell because the reader is scrolling; on a sheet of paper the
   reader is counting rows, and nineteen collections over two pages when
   thirty-five fit one is a second sheet spent on air. Tight rows, and the
   line-height comes down with them — including the extra leading Bangla is
   given on screen, which at 10.5px is more than the script needs. */
.printable .tbl th { padding-top:4px; padding-bottom:4px; line-height:1.3; }
.printable .tbl td { padding-top:3px; padding-bottom:3px; line-height:1.35; }
.printable .lang-bn .tbl th, .printable .lang-bn .tbl td,
.lang-bn .printable .tbl th, .lang-bn .printable .tbl td { line-height:1.45; }
/* The second line under a cell — the ward under the point, the contact note
   under the reporter, "so far" under the duration — is set at 11px inline in
   the views, which on a 10.5px sheet prints *larger* than the row it
   qualifies. !important because those are inline styles and there is nothing
   else to outrank them with. */
.printable .tbl td > div { font-size:9px !important; line-height:1.3; }
/* The "no rows" line is a 36px-tall band on a sheet that has nothing else on
   it to justify the space. */
.printable .tbl__empty { padding:12px 9px; }
.printable .table-wrap { overflow:visible; }
.printable .table-wrap > .tbl { min-width:0 !important; table-layout:auto; }
.printable .tbl .nowrap, .printable .tbl td, .printable .tbl th {
  /* break-word, not anywhere: `anywhere` lets the browser count a column's
     minimum as one character, and a table squeezed under that rule split
     "Momena" across two lines while there was still room at the spaces. */
  white-space:normal; overflow-wrap:break-word;
}
.printable .tbl td.num, .printable .tbl th.num { white-space:nowrap; overflow-wrap:normal; }
/* On screen a status badge is one line and clips with an ellipsis rather than
   widening its column; on paper an ellipsis is a word nobody can recover, and
   ten such columns held the complaints register wider than the sheet. They wrap
   here instead, which is what lets the last column onto the page. */
.printable .badge, .printable .span-pill,
.printable .cat-chip, .printable .cat-chip__text {
  white-space:normal; overflow:visible; text-overflow:clip; max-width:none;
}
.sheet { max-width:920px; margin:0 auto; padding:26px 24px; color:#0f172a; }
.sheet__head { display:flex; align-items:flex-start; gap:18px; border-bottom:2px solid var(--brand-600);
  padding-bottom:14px; margin-bottom:18px; }
.sheet__org { font-size:12px; letter-spacing:.06em; text-transform:uppercase; color:var(--brand-700); font-weight:700; }
.sheet__title { margin:3px 0 5px; font-size:22px; }
.sheet__meta { font-size:12.5px; color:#475569; }
.sheet__brand { margin-left:auto; color:var(--brand-600); }
.sheet__table { width:100%; border-collapse:collapse; font-size:12px; }
.sheet__scroll { overflow-x:auto; }
@media print { .sheet__scroll { overflow:visible; } }
.sheet__table th {
  text-align:left; padding:8px 9px; background:var(--brand-50); color:var(--brand-800);
  border-bottom:1.5px solid var(--brand-500); font-size:11px; text-transform:uppercase; letter-spacing:.03em;
}
.sheet__table td { padding:7px 9px; border-bottom:1px solid #e2e8f0; vertical-align:top; }
.sheet__table .num { text-align:right; }
.sheet__table tfoot td { font-weight:700; border-top:2px solid #94a3b8; border-bottom:0; }
.sheet__table tbody tr:nth-child(even) { background:#f8fafc; }
.sheet__foot { margin-top:16px; padding-top:10px; border-top:1px solid #e2e8f0;
  font-size:11px; color:#64748b; }
.sheet__hint { margin-top:14px; padding:10px 12px; border-radius:8px;
  background:var(--info-soft); color:#075985; font-size:12.5px; text-align:center; }

@media print {
  @page { size:A4; margin:14mm 12mm; }
  .no-print { display:none !important; }
  .sheet { max-width:none; padding:0; }
  .sheet__table { font-size:10.5px; }
  .sheet__table thead { display:table-header-group; }   /* repeat on every page */
  .sheet__table tr { break-inside:avoid; }
  .sheet__foot { position:running(footer); }
  a[href]::after { content:none !important; }
}

/* Profile photo: one round frame whether it holds a picture or initials, so the
   card does not resize the moment somebody uploads one. */
.avatar-edit { position:relative; width:128px; height:128px; margin:0 auto; }
.avatar-edit__img, .avatar-edit__fallback {
  width:128px; height:128px; border-radius:50%; object-fit:cover; display:grid; place-items:center;
  border:3px solid var(--surface); box-shadow:0 0 0 1px var(--line), var(--shadow);
}
.avatar-edit__fallback {
  background:linear-gradient(140deg,var(--brand-600),var(--brand-800)); color:#fff;
  font-size:38px; font-weight:700; letter-spacing:.02em;
}
.avatar-edit__img { position:absolute; inset:0; background:var(--surface-2); }
.avatar--photo { padding:0; overflow:hidden; }
.avatar--photo img { width:100%; height:100%; object-fit:cover; display:block; }

/* ---------------- modal ----------------
   A native <dialog>, so Escape, the backdrop and focus containment come from the
   browser rather than from hand-written key handlers. Used for the short
   create/edit forms where a whole page navigation is more ceremony than the task
   deserves — adding a ward should not lose your place in the list. */
dialog.modal {
  width:min(680px,calc(100vw - 28px)); max-height:calc(100vh - 60px); padding:0;
  border:1px solid var(--line); border-radius:var(--radius); background:var(--surface);
  color:var(--text); box-shadow:var(--shadow-lg); overflow:visible;
}
dialog.modal::backdrop { background:rgba(15,23,42,.45); }
.modal__head {
  display:flex; align-items:center; gap:10px; padding:14px 16px;
  border-bottom:1px solid var(--line);
}
.modal__head h2, .modal__head h3 { margin:0; font-size:16px; }
.modal__close {
  margin-left:auto; width:32px; height:32px; border-radius:8px; border:0; cursor:pointer;
  background:var(--surface-2); color:var(--text-2); font-size:15px;
}
.modal__close:hover { background:var(--bad-soft); color:var(--bad); }
.modal__body { padding:16px; overflow-y:auto; max-height:min(64vh,560px); }
/* The form grid's column counts are keyed to the viewport, but a dialog is
   680px wide whatever the viewport is — on a wide screen that gave a modal
   four 130px fields side by side. Inside one, fit to the dialog instead. */
.modal__body .form-grid { grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); }
.modal__foot {
  display:flex; gap:9px; padding:12px 16px; border-top:1px solid var(--line);
  background:var(--surface-2); border-radius:0 0 var(--radius) var(--radius);
}
.modal__foot .btn--danger { margin-left:auto; }
/* One arrangement for every dialog: the actions on the right, a destructive
   one alone on the left. Before, a dialog was right-aligned only when it
   happened to have a Delete (whose margin pushed everything after it over)
   and left-aligned when it did not. */
.modal__foot { justify-content:flex-end; }
.modal__foot > .btn--danger:first-child { margin-left:0; margin-right:auto; }
@media (max-width:560px) {
  dialog.modal:not(.modal--confirm):not(.modal--done):not(.modal--peek) {
    width:100vw; max-width:100vw; max-height:100vh; height:100vh; border-radius:0; border:0;
  }
  dialog.modal--peek { width:calc(100vw - 24px); max-width:none; height:fit-content;
    max-height:calc(100vh - 40px); border-radius:var(--radius); border:1px solid var(--line); }
  /* The phone override for .modal__body lives with the dvh sizing further down,
     which is the later of the two and therefore the one that decides. Declaring
     it here as well only meant a reader had to find both to know the answer. */
}

/* A quick look at one row: read-only, so it is a card rather than a full-screen
   sheet, and it stops growing before it becomes a page — anything longer than
   this belongs on the record itself. */
dialog.modal--peek { width:min(620px,calc(100vw - 28px)); }

/* The same panel, down the right-hand edge instead of centred. A conversation
   is read beside the list it came from — the list stays visible, in place, and
   the panel is the full height of the screen so a long thread has the room a
   centred card cannot give it without covering everything.

   It is still a <dialog>, so Escape, the backdrop and the focus trap are the
   browser's own and not this stylesheet's problem. */
dialog.modal--side {
  /* The height lives with the other dvh sizing further down, which is a later
     rule and would otherwise win. */
  margin:0 0 0 auto;
  width:min(560px,100vw); max-width:100vw;
  border-radius:0; border-inline-end:0; border-block:0;
  box-shadow:-18px 0 40px -24px rgba(15,23,42,.45);
  display:flex; flex-direction:column;
}
/* Pinned open: the panel stops being a modal and becomes part of the page.
   No backdrop, no focus trap, the list still live beside it — and the content
   is given the width back by `.has-docked-panel` below rather than sliding
   underneath, which is the difference between a panel and a cover. */
dialog.modal--docked {
  /* Below the topbar, not over it: docked is a lasting state, and a panel that
     covers the search box and the bell for as long as it is open is a panel
     that takes the application away to show one message. 58px is the topbar. */
  inset-block:58px 0; inset-inline-end:0; inset-inline-start:auto;
  position:fixed; z-index:39; height:auto; max-height:none;
  border-inline-start:1px solid var(--line);
  box-shadow:-14px 0 34px -26px rgba(15,23,42,.4);
}
dialog.modal--docked::backdrop { background:none; }
body.has-docked-panel .content {
  padding-inline-end:calc(560px + 20px);
  /* The content is normally capped and centred; with a panel down one side that
     centring put a 66px gap between the list and the panel on a wide screen and
     20px on a narrow one — the same layout looking misaligned at one size and
     not the other. Docked, it uses the width it has, up to the panel. */
  max-width:none; margin-inline:0;
}
body.has-docked-panel .flash-stack { inset-inline-end:calc(560px + 16px); }
/* Two pinned things on one screen: the conversation panel holds the right-hand
   strip, so a pinned card steps aside rather than sitting on top of it — the
   composer was covering the panel's reply box. */
body.has-docked-panel .card--pinned { inset-inline-end:calc(560px + 16px); }
@media (max-width:1359px) { body.has-docked-panel .card--pinned { inset-inline-end:16px; } }
@media (max-width:1359px) { body.has-docked-panel .content { padding-inline-end:20px; max-width:1560px; margin-inline:auto; } }
/* The pin belongs with the close button at the end of the head, not next to the
   title — the two of them are what you can do to the panel. */
.modal__head .modal__pin { margin-inline-start:auto; }
.modal__head .modal__pin + .modal__close { margin-inline-start:6px; }
.modal__pin.is-on { background:var(--brand-50); color:var(--brand-700); }
.modal__pin:hover { background:var(--brand-50); color:var(--brand-700); }

dialog.modal--side[open] { animation:panel-in .18s ease-out; }
/* The panel is a column: the conversation takes the height and scrolls, the
   reply box sits at the bottom of the screen where the next line goes. Without
   this the thread bunched at the top and the reply box floated in the middle
   with a third of the panel empty under it. */
dialog.modal--side .modal__body { max-height:none; flex:1; display:flex; flex-direction:column; min-height:0; }
dialog.modal--side .thread--pane { max-height:none; overflow-y:auto; flex:1; min-height:0; }
dialog.modal--side .modal__body > form { margin-top:auto; }
dialog.modal--side .peek__foot { margin-top:0; }
@keyframes panel-in { from { transform:translateX(18px); opacity:.4; } }
@media (prefers-reduced-motion:reduce) { dialog.modal--side[open] { animation:none; } }
@media (max-width:560px) {
  dialog.modal--side { width:100vw; }
}
dialog.modal--peek .modal__body { max-height:min(70vh,620px); }
.peek__foot { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px;
  padding-top:12px; border-top:1px solid var(--line); }

/* The confirmation is short, so it stays a centred card at every width rather
   than becoming a full-screen sheet like the editing modals. */
dialog.modal--confirm { width:min(440px,calc(100vw - 28px)); }
.confirm__text { margin:0; font-size:14.5px; line-height:1.6; color:var(--text-2); }
.confirm__foot { justify-content:flex-end; }
.confirm__foot .btn { min-height:44px; min-width:112px; justify-content:center; }
@media (max-width:560px) {
  dialog.modal--confirm { width:calc(100vw - 24px); max-width:none; height:fit-content; max-height:calc(100vh - 40px);
    border-radius:var(--radius); border:1px solid var(--line); }
  .confirm__foot { flex-direction:column-reverse; }
  .confirm__foot .btn { width:100%; }
}

                 

.card__head { min-height:0; }
.card__head h2, .card__head h3 { min-width:0; overflow:hidden; text-overflow:ellipsis; }

/* ---- photo evidence ---- */
.photo-actions { display:grid; grid-template-columns:1fr 1fr; gap:9px; margin-bottom:12px; }
.photo-actions .btn { margin:0; min-height:50px; font-size:14px; }
@media (max-width:420px) { .photo-actions { grid-template-columns:1fr; } }

/* The pending selection. This is where somebody checks they picked the right
   photograph before uploading it, so it is drawn larger than the thumbnails
   that follow — and larger than it was, because a 96px square of a person at
   arm's length is not something you can check anything against. */
.photo-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:11px; }
.photo-grid:empty { display:none; }
.photo-cell {
  position:relative; aspect-ratio:1; border-radius:var(--radius-sm); overflow:hidden;
  border:1px solid var(--line); background:var(--surface-2);
}
.photo-cell img { width:100%; height:100%; object-fit:cover; display:block; }
/* A chosen picture is a link to itself at full size (photo-picker.ejs). */
.photo-cell__view { display:block; width:100%; height:100%; cursor:zoom-in; position:relative; }
.photo-cell__view:focus-visible { outline:2px solid var(--brand-500); outline-offset:2px; }
.photo-cell__zoom {
  position:absolute; left:5px; bottom:5px; width:22px; height:22px; border-radius:50%;
  background:rgba(15,23,42,.62); color:#fff; display:grid; place-items:center; pointer-events:none;
}
.photo-cell__remove {
  position:absolute; top:4px; right:4px; width:28px; height:28px; border-radius:50%;
  border:0; cursor:pointer; font-size:13px; line-height:1;
  background:rgba(15,23,42,.62); color:#fff;
}
.photo-cell__remove:active { background:var(--bad); }
/* Dashed "add more" tile that sits alongside the thumbnails (design screen 4). */
.photo-cell--add {
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px;
  border:1.5px dashed var(--line-strong); background:var(--surface); cursor:pointer;
  color:var(--text-3); font-size:22px; line-height:1;
}
.photo-cell--add:hover { border-color:var(--brand-500); color:var(--brand-700); }
.photo-cell__add-label { font-size:11px; }

/* Photo evidence as shown back in the desktop record. */
.photo-strip { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; }
.photo-strip a {
  display:block; aspect-ratio:4/3; border-radius:var(--radius-sm); overflow:hidden;
  border:1px solid var(--line); background:var(--surface-2);
}
.photo-strip img { width:100%; height:100%; object-fit:cover; display:block; }
.photo-strip a:hover { border-color:var(--brand-500); }
/* A gallery the record owns: each tile carries its own cover flag and, for
   anyone who may edit, a bar with "make cover" and remove. */
.photo-strip--gallery { grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); }
.photo-tile { position:relative; margin:0; }
.photo-tile__flag {
  position:absolute; top:6px; left:6px; padding:1px 7px; border-radius:999px;
  background:rgba(15,23,42,.72); color:#fff; font-size:10.5px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; pointer-events:none;
}
.photo-tile__bar { display:flex; gap:4px; align-items:center; justify-content:flex-end; margin-top:4px; }
/* When the picture was taken, beside the button that removes it: on a
   before/after pair the date is what tells the two apart at a glance. */
.photo-tile__when { margin-right:auto; font-size:11px; color:var(--text-3); }
.photo-tile__bar form { margin:0; }
.photo-tile__btn {
  height:24px; padding:0 8px; border-radius:6px; border:1px solid var(--line); cursor:pointer;
  background:var(--surface); color:var(--text-2); font-family:inherit; font-size:11.5px; font-weight:600;
  display:inline-flex; align-items:center; gap:4px;
}
.photo-tile__btn:hover { border-color:var(--brand-500); color:var(--brand-700); }
.photo-tile__btn--danger { color:var(--bad); border-color:color-mix(in srgb, var(--bad) 30%, var(--line)); }
.photo-tile__btn--danger:hover, .photo-tile__btn--danger:focus-visible { border-color:var(--bad); color:#fff; background:var(--bad); }

/* ---- how long it took ----
   A small pill under a date, saying how long a complaint has been running or
   how long it took to close. Muted while that is unremarkable, amber past
   three days, red past a week, green once it is done — so a page of rows can
   be scanned for the one that has been left, without reading a single date. */
.span-pill {
  display:inline-block; margin-top:3px; padding:1px 7px; border-radius:999px;
  font-size:11px; font-weight:650; letter-spacing:.01em; white-space:nowrap;
  border:1px solid transparent;
}
.span-pill--muted { background:var(--muted-soft); color:var(--text-2); }
.span-pill--ok    { background:var(--ok-soft);   color:#15803d; }
.span-pill--warn  { background:var(--warn-soft); color:#92400e; }
.span-pill--bad   { background:var(--bad-soft);  color:#b91c1c; border-color:#fecaca; }

/* ---- before and after ----
   A complaint's photographs answer two different questions — what was wrong,
   and what the crew left behind — so they are shown as two labelled columns
   rather than one strip in upload order. The columns stack on a handset, where
   side by side would be two 140px squares and neither would be readable. */
.ba { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; }
@media (max-width:640px) { .ba { grid-template-columns:1fr; } }
.ba__col { min-width:0; }
.ba__title {
  display:flex; align-items:center; gap:7px; margin:0 0 9px;
  font-size:11.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--text-3);
}
/* The colour is the whole point of the pair: the problem reads as a warning,
   the finished work as done. */
.ba__title::before {
  content:''; width:8px; height:8px; border-radius:50%; background:var(--warn); flex:none;
}
.ba__col--after .ba__title::before { background:var(--ok); }
.ba__count {
  margin-left:auto; padding:0 7px; border-radius:999px; background:var(--surface-2);
  border:1px solid var(--line); color:var(--text-2); font-size:11px; letter-spacing:0;
}
.ba__empty {
  margin:0; padding:20px 12px; border-radius:var(--radius-sm); text-align:center;
  border:1.5px dashed var(--line); color:var(--text-3); font-size:12.5px;
}

/* Which of the two an upload is. Two cards rather than a select: it is asked
   on a phone, in the street, next to a camera button. */
.ba-pick { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:9px; margin-bottom:12px; }
.ba-pick__opt { margin:0; cursor:pointer; }
.ba-pick__opt input { position:absolute; opacity:0; width:0; height:0; }
.ba-pick__box {
  display:flex; align-items:center; gap:10px; min-height:52px;
  padding:9px 12px; border:1.5px solid var(--line); border-radius:var(--radius-sm);
  background:var(--surface); color:var(--text-2);
}
/* The icon says which of the two this is before the words are read, and takes
   the colour its column has above: the problem as reported is the warning the
   register already draws an open complaint with, the finished work is a tick. */
.ba-pick__icon {
  display:grid; place-items:center; width:32px; height:32px; flex:0 0 32px;
  border-radius:9px; background:var(--surface-2); color:var(--text-3);
}
.ba-pick__text { display:flex; flex-direction:column; gap:1px; min-width:0; }
.ba-pick__box strong { font-size:13.5px; color:var(--text); }
.ba-pick__box em { font-style:normal; font-size:11.5px; color:var(--text-3); }
.ba-pick__opt:has(input:checked) .ba-pick__icon { background:var(--warn); color:#fff; }
.ba-pick__opt:has(input[value=after]:checked) .ba-pick__icon { background:var(--ok); color:#fff; }
.ba-pick__opt:has(input:checked) .ba-pick__box { border-color:var(--warn); background:var(--warn-soft); }
.ba-pick__opt:has(input[value=after]:checked) .ba-pick__box { border-color:var(--ok); background:var(--ok-soft); }
.ba-pick__opt:has(input:focus-visible) .ba-pick__box { outline:2px solid var(--brand-500); outline-offset:2px; }

/* The same pictures again, small, under the reason given in the history — the
   evidence sitting with the change it was filed against. */
/* The same picker the citizen's form uses, a size down: a complaint card and a
   modal are denser than a one-question public page, and a 118px tile would be
   the loudest thing on either. */
.photo-add { margin-top:18px; display:block; }
/* Compact in the office: the switch is a thin bar and the target is one row
   deep with its icon beside the words, not stacked above them. The citizen's
   form keeps the tall tile — it is that page's whole purpose — where here the
   control sits under a gallery, inside a card, next to everything else a
   complaint carries. */
.photo-add .pp-src { margin-bottom:8px; padding:3px; gap:3px; border-radius:10px; }
.photo-add .pp-src__btn { min-height:30px; font-size:12.5px; gap:6px; border-radius:8px; }
.photo-add .pp-src__btn .icon { width:15px; height:15px; }
.photo-add .pp-drop {
  flex-direction:row; min-height:46px; gap:9px; padding:10px 14px;
  font-size:13.5px; border-radius:10px;
}
.photo-add .pp-drop__icon .icon { width:18px; height:18px; }
.photo-add__go { margin-top:12px; }
.photo-add__go[disabled] { opacity:.55; cursor:not-allowed; }
.modal .photo-add { margin-top:0; }

.ba-thumbs { display:flex; flex-wrap:wrap; gap:6px; margin-top:7px; }
.ba-thumbs a {
  display:block; width:56px; height:42px; border-radius:6px; overflow:hidden;
  border:1px solid var(--line); background:var(--surface-2);
}
.ba-thumbs a:hover { border-color:var(--brand-500); }
.ba-thumbs img { width:100%; height:100%; object-fit:cover; display:block; }

/* ---- lightbox ----
   A native <dialog> (app.js) holding one picture at the largest size the
   viewport allows, on a dark backdrop. */
dialog.lightbox {
  border:0; padding:0; background:transparent; max-width:100vw; max-height:100vh;
  width:100vw; height:100vh; margin:0; display:grid; place-items:center; overflow:hidden;
}
dialog.lightbox:not([open]) { display:none; }
dialog.lightbox::backdrop { background:rgba(15,23,42,.88); }
.lightbox__fig { margin:0; max-width:min(96vw, 1400px); max-height:92vh; display:flex; flex-direction:column; align-items:center; gap:10px; }
.lightbox__fig img { max-width:100%; max-height:84vh; object-fit:contain; border-radius:var(--radius-sm); box-shadow:var(--shadow-lg); background:#000; }
.lightbox__cap { color:#e2e8f0; font-size:13px; text-align:center; overflow-wrap:anywhere; }
.lightbox__close, .lightbox__nav {
  position:fixed; z-index:1; border:0; cursor:pointer; border-radius:50%;
  background:rgba(255,255,255,.14); color:#fff; display:grid; place-items:center;
}
.lightbox__close { top:14px; right:14px; width:40px; height:40px; }
.lightbox__nav { top:50%; transform:translateY(-50%); width:44px; height:44px; font-size:30px; line-height:1; padding-bottom:4px; }
.lightbox__nav--prev { left:12px; } .lightbox__nav--next { right:12px; }
.lightbox__close:hover, .lightbox__nav:hover { background:rgba(255,255,255,.28); }
.lightbox--single .lightbox__nav { display:none; }
/* Delete, for a picture still being chosen (photo-picker confirmRemove). */
.lightbox__del {
  display:inline-flex; align-items:center; gap:8px; min-height:44px; padding:0 18px; border-radius:999px;
  border:0; cursor:pointer; background:var(--bad); color:#fff; font:inherit; font-weight:600;
}
.lightbox__del[hidden] { display:none; }
.lightbox__del:hover { background:#b91c1c; }
/* The remove confirmation shows the picture being removed. */
.pp-rm { display:flex; gap:14px; align-items:center; }
.pp-rm__img { width:88px; height:88px; object-fit:cover; border-radius:var(--radius-sm); flex:none; background:var(--surface-2, #f1f5f9); }
.pp-rm__text { margin:0; color:var(--text-2, #475569); }
dialog.modal--confirm { width:min(92vw, 420px); }

/* ---------------- reference lookup ----------------
   The complaints desk's first action. It was drawn oversized — a 48px field at
   17px inside a 10px box — on the reasoning that a reference read down a
   telephone is typed under pressure. In place that bought nothing: it is the
   only control above the tiles, so it is already unmissable at the app's own
   size, and the extra 22px pushed the open-complaint counts below the fold on a
   laptop. So it sits on --control-h like every other single-line control, and
   earns its prominence from position and the full width of the page rather than
   from height. Still one tap target, still the widest thing on the row. */
.lookup {
  display:flex; align-items:center; gap:7px; margin-bottom:12px; padding:4px 5px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:var(--shadow);
}
.lookup__icon { display:grid; place-items:center; color:var(--text-3); flex:0 0 auto; padding-left:5px; }
/* input[type=search] carries the app's control height at a matching
   specificity, so the class alone loses to it. Qualified with the element it
   wins — which is what lets the height below be the one that applies. */
input[type=search].lookup__input {
  flex:1 1 auto; min-width:0; height:var(--control-h); padding:0 4px; font-size:14px;
  border:0; background:transparent; color:var(--text);
}
input[type=search].lookup__input:focus { outline:0; box-shadow:none; }
input[type=search].lookup__input::placeholder { color:var(--text-3); font-size:13px; }
/* The whole bar takes the focus ring, so the box reads as one control rather
   than as an icon, a field and a button that happen to be adjacent. */
.lookup:focus-within { border-color:var(--brand-500); box-shadow:0 0 0 3px rgba(20,168,141,.16); }
/* No height of its own: .btn is already --control-h, and a second number here
   is what made the button and the field disagree by 4px in the first place. */
.lookup__go { padding:0 16px; flex:0 0 auto; }
@media (max-width:560px) {
  .lookup { flex-wrap:wrap; }
  input[type=search].lookup__input { flex:1 1 100%; order:-1; }
  .lookup__icon { display:none; }
  .lookup__go { flex:1 1 auto; }
}

.field__stop {
  display:flex; gap:11px; align-items:center; padding:12px 14px; border-bottom:1px solid var(--line);
  text-decoration:none; color:inherit;
}
.field__stop:last-child { border-bottom:0; }

/* A step heading carries its own icon so the four steps are told apart at a glance. */
.card__icon {
  width:26px; height:26px; flex:0 0 26px; border-radius:8px; display:grid; place-items:center;
  background:var(--brand-50); color:var(--brand-700); font-size:14px;
}

.avatar--lg { width:46px; height:46px; flex:0 0 46px; font-size:16px; }

/* Role checkboxes on the message composer. A row of togglable chips rather than
   a stacked list: five roles in a sidebar column would otherwise push the
   message body off the screen. */
.role-pick { display:flex; flex-wrap:wrap; gap:7px; }
.role-pick__item {
  display:inline-flex; align-items:center; gap:7px; cursor:pointer;
  padding:7px 11px; border:1px solid var(--line); border-radius:999px;
  background:var(--surface); font-size:12.5px; font-weight:600; color:var(--text-2);
}
.role-pick__item:hover { border-color:var(--brand-500); }
.role-pick__item:has(input:checked) {
  background:var(--brand-50); border-color:var(--brand-500); color:var(--brand-800);
}
.role-pick__item input { margin:0; }

/* ---------------- misc helpers ---------------- */
.switch { display:flex; gap:9px; align-items:flex-start; font-size:13px; padding:7px 0; }
.switch input { margin-top:3px; }
.switch__hint { font-size:11.5px; color:var(--text-3); }
.mono-sm { font-family:var(--mono); font-size:11.5px; color:var(--text-3); word-break:break-all; }
.doc-card { display:flex; gap:13px; align-items:flex-start; padding:15px 16px;
  text-decoration:none; color:inherit; height:100%; }
.doc-card__icon { width:38px; height:38px; flex:0 0 38px; border-radius:10px;
  background:var(--brand-50); color:var(--brand-700); display:grid; place-items:center; font-size:18px; }
.doc-card:hover { border-color:var(--brand-500); }

@media print {
  .sidebar, .topbar, .page-head__actions, .filters, .pagination, .pager { display:none !important; }
  body { background:#fff; }
  .card { box-shadow:none; border-color:#ddd; break-inside:avoid; }
  /* Paper has no buttons on it. A register printed for a meeting was carrying
     a column of "Status" and "Edit" controls down the right-hand side, which
     is ink spent on things nobody can press — and the same goes for a search
     box or a date picker that survived into the printout. The preview strips
     these from its clone (app.js); this is the same rule for the browser's own
     Ctrl+P, which never sees that clone. */
  .btn, .btn-group, .lookup, .chart-range, .sort-select { display:none !important; }
  input, select, textarea { display:none !important; }
  /* The empty cells those leave behind should not still claim a column. */
  .tbl th:empty, .tbl td:empty { padding:0; width:0; border:0; }
  /* A table that scrolls sideways on screen has to print whole — and fit.
     Made visible but left at its screen width, the fleet register ran off the
     right-hand edge of the paper and the last three columns simply were not
     there. On paper the reader cannot scroll, so the cells wrap instead: a
     registration number over two lines is readable, a missing column is not. */
  .table-wrap { overflow:visible !important; }
  /* The min-widths that make a wide register scroll on screen (see
     .table-wrap > .tbl:has(...) above) have to go on paper, or the table keeps
     asking for 1360px inside a 12mm margin and the last columns fall off the
     sheet. Wrapping cells is the price, and a two-line registration number
     beats a missing column. */
  .table-wrap > .tbl { min-width:0 !important; }
  .tbl { width:100%; table-layout:auto; font-size:10.5px; }
  /* The same ruled grid and the same tight rows the preview shows (see
     .printable--doc and .printable above) — this is the path the browser's own
     Ctrl+P takes, which never sees that clone, and the two have to agree or
     the sheet somebody previewed is not the sheet they get. */
  .tbl { border:1px solid #000; }
  .tbl th {
    border:1px solid #9aa1aa; border-top:0; border-bottom:1px solid #000;
    padding:4px 7px; line-height:1.3;
  }
  .tbl td { border:1px solid #9aa1aa; padding:3px 7px; line-height:1.35; }
  .tbl tbody tr:last-child td { border-bottom:1px solid #9aa1aa; }
  /* The inset line a sticky heading draws under itself lands on the rule under
     the headings; see the same pair in .printable--doc. */
  .tbl thead th { box-shadow:none; }
  .card:has(> .card__body--flush > .table-wrap) { border:0; }
  .lang-bn .tbl th, .lang-bn .tbl td { line-height:1.45; }
  .tbl td > div { font-size:9px !important; line-height:1.3; }
  .tbl__empty { padding:12px 9px; }
  .tbl .nowrap, .tbl td, .tbl th { white-space:normal !important; overflow-wrap:anywhere; }
  /* ...except the figures. Left to wrap anywhere, a serial column printed row
     ten as "1" over "0", and a tonnage as a digit per line. */
  .tbl td.num, .tbl th.num { white-space:nowrap !important; overflow-wrap:normal; }
  .tbl thead { display:table-header-group; }
  .tbl tr { break-inside:avoid; }
}

/* ---------------- touch input & the on-screen keyboard ----------------
   Audited on a phone-sized viewport with a keyboard open. Four separate
   problems, all of which look to a user like "the form jumps about". */

@media (pointer:coarse) {
  /* 1. iOS Safari zooms the whole page in when you focus a control whose text is
        under 16px, and it does not zoom back out when you leave. The form was at
        13.5px and the field app at 15.5px, so every tap on every input shifted
        the layout sideways. 16px is the threshold, not a preference. */
  input[type=text],input[type=email],input[type=password],input[type=number],input[type=date],
  input[type=time],input[type=datetime-local],input[type=search],input[type=tel],select,textarea,
  .select2-container--default .select2-search--dropdown .select2-search__field,
  .select2-container--default .select2-search--inline .select2-search__field,
  .fieldapp input, .fieldapp select, .fieldapp textarea {
    font-size:16px;
  }
  /* Comfortable targets to go with the larger text. */
  input[type=text],input[type=email],input[type=password],input[type=number],input[type=date],
  input[type=time],input[type=datetime-local],input[type=search],input[type=tel],select,textarea {
    padding:10px 12px; min-height:44px;
  }
  textarea { min-height:88px; }
  /* The dropdowns grow with the inputs beside them — through the variable, so
     the text, chevron and clear button move to the new centre with the box.
     (A min-height here with a 24px line-height left the text pinned to the
     top of a 44px control on every phone and tablet.) The field app keeps
     its own 50px, set on a longer selector above. */
  .select2-container--default { --control-h:44px; }
  .select2-container--default .select2-selection--single .select2-selection__rendered { font-size:16px; }
}

input, select, textarea, .select2-container { scroll-margin-block:90px; }

/* 4. Heights that must follow the *visible* viewport rather than the theoretical
      one. 100vh on a phone is the height with the browser chrome hidden and
      ignores the keyboard entirely, so a "full height" panel is taller than the
      screen and its footer is unreachable. dvh tracks both. The vh line stays
      first as the fallback for browsers without dvh. */
.app { min-height:100vh; min-height:100dvh; }

.auth { min-height:100vh; min-height:100dvh; }
dialog.modal { max-height:calc(100vh - 60px); max-height:calc(100dvh - 60px); }
/* The side panel is the exception to the line above: it is the full height of
   the screen by design, and being a later rule that one was quietly clipping it
   sixty pixels short. Kept here, beside the rule it overrides, rather than
   argued with by specificity three hundred lines earlier. */
dialog.modal--side { height:100vh; max-height:100vh; height:100dvh; max-height:100dvh; }
/* Docked it is not the full height of the screen but the height of what is left
   under the topbar, which its own insets already describe — so the line above
   must not hand it a height as well, or it hangs 58px off the bottom. */
dialog.modal--docked { height:auto; max-height:none; }
.modal__body { max-height:min(64vh,560px); max-height:min(64dvh,560px); }
dialog.modal--side .modal__body { max-height:none; }
@media (max-width:560px) {
  /* Full-screen is right for a modal holding a form, and wrong for one holding a
     sentence — a confirmation, an alert or the saved message would be stretched
     to the whole screen around two lines of text. Those variants size to their
     content; everything else fills the screen. */
  dialog.modal:not(.modal--confirm):not(.modal--done):not(.modal--peek) {
    max-height:100vh; height:100vh; max-height:100dvh; height:100dvh;
  }
  dialog.modal--confirm, dialog.modal--done { height:fit-content; }
  /* The sheet's own scroller, not the page behind it, absorbs the keyboard. */
  .modal__body { max-height:none; flex:1; min-height:0; }
  /* [open] only — a bare `display:flex` here would override the closed
     dialog's own display:none and leave every modal on screen. */
  dialog.modal[open] { display:flex; flex-direction:column; }
  /* Most dialogs wrap head, body and foot in a <form>, and the flex column
     above stopped at it: the body could not stretch, so on a phone the action
     bar floated half-way down a full-screen sheet with blank space under it.
     The form carries the column on. */
  dialog.modal[open] > form { display:flex; flex-direction:column; flex:1 1 auto; min-height:0; }
  .modal__foot { margin-top:auto; }
}

/* ---------------- print preview ----------------
   The browser's print dialog is a full-screen takeover that arrives with no
   warning and, on a phone, is awkward to back out of. Every Print button now
   opens this instead: the printable content rendered in an iframe at the paper's
   proportions, with Print and Cancel. What is on screen is what the printer
   gets, because the frame carries the same stylesheets. */
dialog.modal--preview {
  width:min(940px, calc(100vw - 28px));
  max-height:calc(100vh - 48px); max-height:calc(100dvh - 48px);
  display:none; flex-direction:column; padding:0;
}
dialog.modal--preview[open] { display:flex; }
.preview__stage {
  flex:1; min-height:0; background:var(--surface-2); padding:14px;
  overflow:auto; display:flex; align-items:flex-start;
  /* `safe` — a centred flex item wider than its container overflows on *both*
     sides and the left half cannot be scrolled to, which is exactly what a
     sheet at actual size on a phone is. The plain value stays first for
     browsers that do not know the keyword. */
  justify-content:center; justify-content:safe center;
}
/* The sheet is always A4 at 96dpi and is scaled to the room there is (app.js
   sets the variable). A transform leaves the original box in the layout, so
   the two margins give back the width and height the scale took off — without
   them the stage keeps scrollbars for a sheet that is no longer there. */
.preview__fit {
  --preview-scale:1;
  flex:0 0 auto; width:794px;
  transform:scale(var(--preview-scale)); transform-origin:top left;
  margin-inline-end:calc(794px * (var(--preview-scale) - 1));
  margin-bottom:calc(1123px * (var(--preview-scale) - 1));
}
.preview__frame {
  /* A4 at 96dpi. The height grows to the content (app.js) so a long register
     is one sheet the stage scrolls, not a page-tall box with a scrollbar of
     its own that a thumb cannot reach. */
  width:794px; height:1123px; border:1px solid var(--line); border-radius:var(--radius-sm);
  background:#fff; box-shadow:var(--shadow); display:block;
}
.preview__title { margin:0; font-size:15px; font-weight:650; }
/* Right-hand end of the header, beside the close button — which has to be told
   to stop claiming the free space for itself, the same way the pin does. */
.preview__zoom { margin-inline-start:auto; flex:0 0 auto; }
/* :not([hidden]) — on a screen the sheet fits, the toggle is not there to
   take the free space, and the close button has to go back to claiming it or
   it sits against the title. */
.preview__zoom:not([hidden]) + .modal__close { margin-inline-start:6px; }
@media (max-width:560px) {
  dialog.modal--preview {
    width:100vw; max-width:100vw; border-radius:0; border:0;
    height:100dvh; max-height:100dvh;
  }
  .preview__stage { padding:8px; }
  .preview__frame { border-radius:0; }
}

/* ---------------- the preview as a document ----------------
   A register on screen is colour-coded on purpose: a green "completed", an
   amber pill for a complaint that has run long, a tinted tile per measure.
   None of that survives contact with an office printer — it comes out as grey
   blocks behind grey text — and a printed register is a document, read as
   rows of words, not a dashboard. So the clone (never the app, and never the
   designed report sheets, which is why this is its own class) is rendered in
   ink on paper: one colour, hairline rules, and the badges reduced to the
   words they carry. */
.printable--doc {
  --brand-900:#000; --brand-800:#000; --brand-700:#000;
  --brand-600:#000; --brand-500:#000; --brand-100:#fff; --brand-50:#fff;
  --accent:#000; --accent-soft:transparent;
  --ok:#000; --info:#000; --warn:#000; --bad:#000;
  --ok-soft:transparent; --info-soft:transparent; --warn-soft:transparent;
  --bad-soft:transparent; --muted-soft:transparent;
  --bg:#fff; --surface:#fff; --surface-2:#fff;
  --line:#c8ccd2; --line-strong:#9aa1aa;
  --text:#000; --text-2:#333; --text-3:#555;
  --shadow:none; --shadow-lg:none;
  color:#000;
}
/* A link is a destination nobody can follow on paper: the reference, the date,
   the point's name are simply the row's text. */
.printable--doc a { color:inherit; text-decoration:none; }
.printable--doc .card, .printable--doc .kpi { box-shadow:none; background:#fff; }
/* The status vocabulary as words. A pill with its tint removed is a word with
   odd padding around it, so the shape goes too. */
/* .cat-chip too: a waste-type tag is the same thing as a badge wearing a
   different class name, and being missed here it printed as an outlined pill
   in colour — a rounded border inside a square cell — with the tag's name
   clipped to an ellipsis, which on paper is a word nobody can recover. */
.printable--doc .badge, .printable--doc .span-pill, .printable--doc .chip,
.printable--doc .cat-chip {
  background:none; color:inherit; border:0; border-radius:0;
  padding:0; font-size:inherit; font-weight:600;
}
/* The sheet's own heading (app.js builds it; it exists only in the clone).
   A letterhead, not a page title: who the sheet is from, what register it is,
   what it was narrowed to and when it was taken — the four things somebody
   holding it at a meeting has no other way to find out. Set small and ruled
   off, because it is the answer to "what is this", not the content. */
/* No rule of its own. The table's top edge is nine pixels below and draws one
   already, and the two together read as a doubled border rather than as a
   letterhead and a register: the gap does the separating instead. */
.printable .printhead { margin-bottom:11px; }
.printable .printhead__org {
  font-size:8.5px; letter-spacing:.09em; text-transform:uppercase; font-weight:700;
}
.printable .printhead__title { margin:2px 0 0; font-size:16px; line-height:1.2; }
.printable .printhead__sub { font-size:10.5px; margin-top:2px; }
.printable .printhead__meta { font-size:9px; color:#333; margin-top:2px; line-height:1.35; }
/* The heading belongs to the first page. Repeating it above every sheet is the
   table header's job, and that already repeats. */
@media print { .printhead { break-after:avoid; } }

/* A register on screen is rows separated by hairlines, with no frame and no
   column rules: the card around it draws the box and the whitespace does the
   rest. Printed, that falls apart — the sheet has no card, so the register
   ends up as text floating on paper with nothing holding the columns apart,
   and a reader following a quantity across nine columns loses the row. On
   paper a register is a ruled grid: a frame, a rule between every column and
   every row, and a heavier line under the headings. */
/* One weight per job, and each line drawn once. The frame is the only black
   box; inside it every column rule is the same grey hairline, headings and
   rows alike. Boxing the heading cells in black as well put a second black
   rule under the frame and a third under the thead — three heavy lines in the
   top thirty pixels of the sheet, and header columns visibly thicker than the
   body columns they line up with. */
.printable--doc .tbl { border:1px solid #000; }
/* Two tags in one cell were told apart by their pill shapes; without them
   "Recyclable Hazardous" reads as one tag with a long name. */
.printable--doc .cat-chip + .cat-chip::before { content:"· "; }
.printable--doc .tbl th {
  background:none; color:#000; letter-spacing:.02em;
  border:1px solid #9aa1aa;
  /* The frame is the top edge; the heading needs no second line above it. */
  border-top:0;
  /* The one line that is the heading's own: where the headings stop and the
     register starts. */
  border-bottom:1px solid #000;
}
.printable--doc .tbl td { border:1px solid #9aa1aa; }
/* On screen the last row drops its rule because the card's edge closes the
   table off. Here nothing does, and the grid was left open along the bottom. */
.printable--doc .tbl tbody tr:last-child td { border-bottom:1px solid #9aa1aa; }
/* The cursor happens to be over the frame when the modal opens, and a tinted
   row in a printout is a row somebody will ask about. */
.printable--doc .tbl tbody tr:hover { background:none; }
/* A measure on a tile keeps its figure and its label; the wash behind it, the
   coloured rule along the top and the white-on-colour glyph do not print as
   anything but grey. */
.printable--doc .kpi { --tone:#000; border:1px solid var(--line); }
.printable--doc .kpi::after { display:none; }
.printable--doc .kpi__icon { display:none; }
.printable--doc .kpi__label { padding-right:0; color:#333; }
.printable--doc .kpi__value { color:#000; }
/* app.js pins a wide register's action column to the right edge on screen. The
   clone has no action column left, so the pin would fall on the last real one
   and print its shadow as a grey smudge down the page. */
.printable--doc .tbl--sticky-actions th:last-child,
.printable--doc .tbl--sticky-actions td:last-child {
  position:static; background:none;
}
/* Every inset shadow a cell carries on screen, and for the same reason: on a
   screen it is a soft edge, in ink it is a second rule drawn a hairline inside
   the first. Two of them: the coloured bar down the first cell of a row that
   is late or wrong (which flattens to black here, so it says nothing the row's
   own words do not), and the line a sticky heading draws under itself, which
   lands directly on the black rule under the headings. */
.printable--doc .tbl th, .printable--doc .tbl td { box-shadow:none; }
/* A register inside a card, flush to its edges — the reports page — printed
   the card's rounded grey border hard against the table's square black frame:
   two borders, a pixel apart, disagreeing about the corner. On paper the table
   draws its own box, so the card stops drawing one. Only where they collide:
   a card holding text or a chart keeps its frame. */
.printable--doc .card:has(> .card__body--flush > .table-wrap) {
  border:0; border-radius:0;
}

@supports not (background: color-mix(in srgb, red 50%, #fff)) {
  .bar__fill--pending { background:var(--warn); }
}

/* A link inside a sentence is 17px tall, which is fine to read and hard to hit
   with a thumb. The negative margin lets the hit area grow past the line box
   without pushing the surrounding text apart. */
/* :not(.btn) — a notice can hold a button as well as a sentence, and this rule
   was underlining it and stacking its own padding on top of the button's. */
.notice a:not(.btn), .field__note a:not(.btn) {
  display:inline-block; padding:8px 5px; margin:-8px -5px;
  text-decoration:underline; text-underline-offset:2px; font-weight:600;
}

/* ---------------- public complaint page ----------------
   The one screen a citizen sees. It is not the office app: no sidebar, no
   topbar, no register. Somebody is standing next to an overflowing bin holding
   a phone in one hand, so it is a single column at any width, the targets are
   thumb-sized, and the send button is the largest thing on the page.

   Capped at 560px rather than stretched: on a desktop this is still one form
   for one person, and a full-width text box across a 27-inch monitor reads as
   an admin screen somebody wandered into by mistake. */
.report { min-height:100vh; min-height:100dvh; background:var(--surface-2); }
.report__head {
  display:flex; align-items:center; gap:12px; padding:12px 16px;
  background:linear-gradient(135deg, #075045, #0f8a74); color:#fff;
  padding-top:calc(12px + env(safe-area-inset-top, 0px));
}
.report__brand { display:flex; align-items:center; gap:10px; min-width:0; margin-right:auto; }
.report__mark {
  width:36px; height:36px; flex:0 0 36px; border-radius:10px; display:grid; place-items:center;
  background:rgba(255,255,255,.16);
}
.report__org { font-weight:700; font-size:14.5px; line-height:1.3; }
.report__sub { font-size:11.5px; opacity:.85; }
.langswitch--report {
  min-height:36px; background:transparent; border-color:rgba(255,255,255,.4); color:inherit;
}
.langswitch--report:hover { border-color:#fff; color:#fff; }

.report__body {
  max-width:560px; margin:0 auto; padding:20px 16px calc(40px + env(safe-area-inset-bottom, 0px));
}
.report__title { font-size:22px; margin:0 0 6px; }
.report__lead { color:var(--text-2); font-size:14.5px; line-height:1.6; margin:0 0 18px; }

/* What the QR code already knew, shown rather than asked. */
.report__place {
  display:flex; gap:11px; align-items:flex-start; margin-bottom:18px; padding:12px 14px;
  background:var(--brand-50); border:1px solid var(--brand-100); border-radius:var(--radius);
}
.report__place-icon { color:var(--brand-700); display:grid; place-items:center; flex:0 0 auto; }
.report__place-name { font-weight:650; color:var(--brand-900); }
.report__place-sub { font-size:12.5px; color:var(--brand-800); margin-top:2px; }
.report__place-sub code { background:rgba(255,255,255,.6); }

.report__form { display:grid; gap:14px; }
.report__group { border:0; padding:0; margin:0; }
/* The two questions that must be answered are the two that carry a legend or a
   label with no "optional" beside them. Marking the required ones with an
   asterisk and the optional ones with a word said the same thing twice, in two
   notations, one of which means nothing to most readers. */
.report__legend { font-size:14px; font-weight:650; color:var(--text); padding:0; margin-bottom:10px; }

/* The categories, as cards of equal size rather than a dropdown or a row of
   pills. A <select> on a phone opens a wheel that hides the question being
   asked; pills of ragged widths wrapping over three lines have to be read one
   at a time. A grid of equal cards is scanned in a single pass, and each one
   carries the icon and colour its category was given, so most people pick on
   the picture and read the words only to confirm.

   Two columns on a phone, three once there is room. `auto-fit` rather than a
   fixed count, so a municipality that adds a seventh category gets a sensible
   grid without anybody touching this file. */
.opts { display:grid; grid-template-columns:repeat(auto-fit, minmax(148px, 1fr)); gap:10px; }
.opt { position:relative; display:block; }
.opt input {
  position:absolute; inset:0; width:100%; height:100%; margin:0; opacity:0; cursor:pointer;
}
.opt__box {
  --opt:var(--brand-600);
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px;
  min-height:96px; height:100%; padding:14px 10px; text-align:center;
  border:1.5px solid var(--line-strong); border-radius:14px; background:var(--surface);
  color:var(--text); font-size:14px; font-weight:600; line-height:1.3;
  transition:border-color .12s, background .12s, box-shadow .12s;
}
.opt__icon {
  display:grid; place-items:center; width:42px; height:42px; border-radius:12px;
  /* The category's own colour at a tenth, so six cards read as one family
     rather than six competing blocks of colour. */
  background:color-mix(in srgb, var(--opt) 12%, #fff); color:var(--opt);
}
/* color-mix is recent; a browser without it still gets a tinted tile. */
@supports not (background: color-mix(in srgb, red 10%, #fff)) {
  .opt__icon { background:var(--surface-2); }
}
.opt__label { display:block; }
.opt input:checked + .opt__box {
  border-color:var(--opt); background:color-mix(in srgb, var(--opt) 7%, #fff);
  box-shadow:inset 0 0 0 1px var(--opt);
}
.opt input:focus-visible + .opt__box { box-shadow:0 0 0 3px rgba(20,168,141,.3); }
@media (hover:hover) { .opt:hover .opt__box { border-color:var(--opt); } }

/* ---- the steps ----
   Each question is a card with a number, an icon and one line saying why it is
   being asked. Nothing is hidden behind a "next" button — a wizard on a public
   form is three more taps and a chance to lose somebody halfway — but the
   numbering tells a first-time reader how much of this there is, and the icon
   is read by people who will not read the heading at all. */
.rstep {
  padding:16px; border:1px solid var(--line); border-radius:14px;
  background:var(--surface); display:grid; gap:14px;
}
.rstep__head { display:flex; align-items:flex-start; gap:12px; }
.rstep__icon {
  width:40px; height:40px; flex:0 0 40px; border-radius:12px; display:grid; place-items:center;
  background:var(--brand-50); color:var(--brand-700); border:1px solid var(--brand-100);
}
.rstep__text { min-width:0; }
.rstep__step {
  display:block; font-size:11px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
  color:var(--brand-600); margin-bottom:2px;
}
.rstep__title { font-size:16px; font-weight:700; margin:0; line-height:1.35; }
/* A label inside the heading is the heading: the textarea and the ward select
   are labelled by the step's own title rather than by a second line of text
   under it. */
.rstep__title label { font:inherit; color:inherit; margin:0; cursor:pointer; }
.rstep__note { margin:3px 0 0; font-size:12.5px; color:var(--text-2); line-height:1.55; }
.rstep .field { margin:0; }
.rstep .field > label { margin-bottom:6px; }

/* ---- camera or gallery ----
   A switch above one big tile, rather than two buttons side by side. Two
   buttons make somebody read both and choose between two similar sentences;
   a switch makes the tile the only thing to tap, and it opens the camera
   unless they say otherwise — which is what somebody standing in front of an
   overflowing bin wants. The switch only appears on a device that honours
   `capture`; a desktop gets the tile alone, opening the file chooser. */
.pp-src {
  display:grid; grid-template-columns:1fr 1fr; gap:4px; margin-bottom:10px; padding:4px;
  background:var(--surface-2); border:1px solid var(--line); border-radius:12px;
}
.pp-src__btn {
  display:flex; align-items:center; justify-content:center; gap:7px; min-height:40px;
  border:0; border-radius:9px; background:transparent; cursor:pointer;
  font-family:inherit; font-size:13.5px; font-weight:650; color:var(--text-2);
}
.pp-src__btn.is-on { background:var(--surface); color:var(--brand-800); box-shadow:var(--shadow); }
.pp-src__btn:focus-visible { outline:2px solid var(--brand-500); outline-offset:1px; }

.pp-drop {
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px;
  width:100%; min-height:118px; padding:16px 12px;
  border:1.5px dashed var(--line-strong); border-radius:14px; background:var(--surface-2);
  color:var(--brand-800); font-size:14.5px; font-weight:650;
}
.pp-drop__icon { display:grid; place-items:center; color:var(--brand-600); }
/* The in-page camera. A 4:3 well so the dialog does not resize the moment the
   stream arrives and move the shutter out from under a waiting finger. */
.modal--cam { max-width:520px; }
.pp-cam { display:grid; gap:10px; }
.pp-cam video {
  width:100%; aspect-ratio:4/3; object-fit:cover; display:block;
  border-radius:12px; background:#0f172a;
}
.pp-cam video[hidden] { display:none; }
.pp-cam__msg {
  margin:0; padding:16px; border-radius:12px; text-align:center; line-height:1.55;
  background:var(--warn-soft); color:#92400e; font-size:13.5px;
}
.pp-drop__icon .icon { width:26px; height:26px; }
@media (hover:hover) {
  .pp-drop:hover { border-color:var(--brand-500); background:var(--brand-50); }
}

/* The form's labels, one size up from the office app's. This page is read once,
   in the street, by somebody who has never seen it before — where the register
   is read all day by somebody who knows it by heart. */
.report__form .field > label {
  font-size:14px; font-weight:650; color:var(--text);
}
.report__form .hint, .report__form .field__note { font-size:12.5px; }
/* The browser's own select, sized like every other control on the page rather
   than like a filter box in the office app. */
select.report__select {
  height:52px; font-size:16px; padding:0 14px; border-radius:10px;
  border-color:var(--line-strong); background-color:var(--surface);
}
.report__form textarea { min-height:118px; font-size:16px; padding:13px 14px; border-radius:10px; }
/* The address is two lines, not the description's five. */
.report__form #address { min-height:76px; }
.report__form input[type=text], .report__form input[type=tel] {
  height:52px; font-size:16px; padding:0 14px; border-radius:10px;
}

/* The send button rides at the bottom of the screen the whole way down the
   form, the way it would in an app. The negative bottom margin cancels the
   page's own bottom padding so the bar finishes flush with the foot of the
   page instead of floating 40px above it. */
.report__bar {
  position:sticky; bottom:0; z-index:5;
  margin:2px -16px calc(-40px - env(safe-area-inset-bottom, 0px));
  padding:12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  background:var(--surface-2); border-top:1px solid var(--line);
}
@supports (backdrop-filter: blur(6px)) {
  .report__bar { background:color-mix(in srgb, var(--surface-2) 86%, transparent); backdrop-filter:blur(8px); }
}
.report__send {
  width:100%; min-height:52px; font-size:16px; border-radius:12px;
}
.report__privacy { font-size:12px; color:var(--text-3); line-height:1.6; margin:0; }

/* Off-screen, not display:none — a bot that skips hidden inputs still fills a
   field it can see in the markup. */
.report__trap { position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }

.report__body--done { text-align:center; padding-top:36px; }
.report__tick {
  width:66px; height:66px; margin:0 auto 16px; border-radius:50%; display:grid; place-items:center;
  background:var(--ok-soft); color:#15803d;
}
.report__ref {
  margin:18px 0; padding:16px; border-radius:var(--radius);
  background:var(--surface); border:1px dashed var(--brand-500);
}
.report__ref-label { font-size:12px; color:var(--text-3); text-transform:uppercase; letter-spacing:.05em; }
.report__ref-value {
  font-family:var(--mono); font-size:28px; font-weight:700; color:var(--brand-800);
  margin-top:6px; letter-spacing:.02em;
}
.report__again { margin-top:20px; }

/* On a phone the form fills the screen; the card treatment is a desktop
   nicety and would only waste width. */
@media (min-width:600px) {
  .report__body { padding-top:28px; }
}

/* ---------------- QR sheet ----------------
   Cards to print, cut out and fix to a bin or a sign. Sized so four fit an A4
   page with room to cut, and so the code itself stays above 32mm — below that
   a cheap phone camera in low light stops resolving it, which is exactly the
   condition these are read in. */
.qr-sheet { display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:14px; }
.qr-card {
  margin:0; padding:14px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); text-align:center; break-inside:avoid;
}
.qr-card__head { margin-bottom:10px; }
.qr-card__org { font-weight:700; font-size:13px; color:var(--brand-800); }
.qr-card__ask { font-size:12.5px; color:var(--text-2); line-height:1.45; margin-top:2px; }
.qr-card__code { width:100%; max-width:190px; margin:0 auto; }
.qr-card__code svg { width:100%; height:auto; display:block; }
.qr-card__foot { margin-top:10px; }
.qr-card__name { font-weight:650; font-size:13.5px; line-height:1.35; }
.qr-card__meta { font-family:var(--mono); font-size:11.5px; color:var(--text-3); margin-top:3px; }

@media print {
  .no-print { display:none !important; }
  .qr-sheet { grid-template-columns:repeat(2,1fr); gap:10mm; }
  .qr-card { border:1px dashed #999; padding:8mm; }
}

/* ---------------- chart window controls ----------------
   A chart that owns its own date range: preset buttons for the questions asked
   every week, a pair of dates for the one asked once a year, and the resolved
   window spelled out so nobody has to infer it from the axis. Sits under the
   card head rather than in it — three buttons, two date fields and a submit do
   not fit on a heading line at any width worth designing for. */
.chart-range {
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px;
  padding:11px 16px; border-bottom:1px solid var(--line); background:var(--surface-2);
}
.chart-range__dates { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:0; }
.chart-range__field { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--text-2); }
.chart-range__field input[type=date] { width:auto; height:32px; font-size:12.5px; padding:0 8px; }
.chart-range__window { margin-left:auto; font-size:12px; color:var(--text-3); white-space:nowrap; }
@media (max-width:720px) {
  .chart-range__window { margin-left:0; width:100%; }
  .chart-range__field { flex:1 1 46%; }
  .chart-range__field input[type=date] { flex:1 1 auto; min-width:0; }
}

/* ---------------- the general QR code ----------------
   The one somebody printing a single code almost always wants, given its own
   block above the sheet of per-bin codes rather than hidden among them. */
.qr-general { display:flex; flex-wrap:wrap; gap:20px; align-items:center; }
.qr-general__code { width:150px; flex:0 0 150px; }
.qr-general__code svg { width:100%; height:auto; display:block; }
.qr-general__text { flex:1 1 260px; min-width:0; }
.qr-general__text h2 { font-size:16px; margin:0 0 4px; }
.qr-general__text code { overflow-wrap:anywhere; }
/* Under each card on the sheet, on screen only. */
.qr-card__get { margin-top:6px; font-size:11px; color:var(--text-3); display:flex; gap:6px; justify-content:center; }

/* ---------------- sortable table headings ----------------
   A heading that reorders the table is a link, and has to look like one without
   turning the header row into a row of blue text: it inherits the heading's own
   colour and says what it is by the arrow beside it and by moving under the
   cursor. The mark keeps its width whether or not this column is the sorted
   one, so clicking through the headings does not shuffle the columns sideways. */
.tbl th .th-sort {
  color:inherit; display:inline-flex; align-items:center; gap:5px; white-space:nowrap;
}
.tbl th .th-sort:hover { color:var(--brand-700); text-decoration:none; }
.tbl th .th-sort__mark { font-size:9px; min-width:9px; color:var(--brand-600); }

/* ---------------- a tile that is also the filter it applies ----------------
   A row of KPI tiles that are links has two states worth drawing and drew
   neither: which one the reader is currently looking through, and which ones
   are counting nothing. The register's tiles are its status filter, so landing
   on ?status=overdue with no tile marked left the reader to work out from the
   URL why the table had shrunk. */
.kpi.is-on {
  border-color:var(--tone); box-shadow:0 0 0 2px color-mix(in srgb, var(--tone) 30%, transparent);
}
.kpi.is-on::after { height:5px; }
/* Nothing overdue is good news, and a scarlet tile reading 0 says the opposite
   every time somebody opens a healthy register. An empty count keeps its place
   in the row and gives up its alarm. */
.kpi.is-zero { --tone:#94a3b8; --tone-soft:var(--surface-2); }
.kpi.is-zero .kpi__value { color:var(--text-3); }
.kpi.is-zero .kpi__icon { box-shadow:none; opacity:.75; }
/* Money is a longer token than a count and sets its own size rather than each
   view repeating an inline override. `--unit` is the same problem with a
   different tail: a tonnage carries its unit inside the figure, so "১২,৪৮০ কেজি"
   is one unbreakable token where a bare count is four characters. */
.kpi--money .kpi__value, .kpi--unit .kpi__value { font-size:clamp(18px, 1.7vw, 22px); }

/* ---------------- ranked bar list ----------------
   The side panels of a register — most expensive, most repaired, busiest —
   were three different hand-rolled stacks of inline flex styles. One component:
   a rank number, a name that links to the thing, its figure, a bar for the
   shape of the distribution and a quiet line of context underneath. */
.rank { display:flex; flex-direction:column; gap:2px; }
.rank__row {
  display:grid; grid-template-columns:20px minmax(0,1fr); gap:10px; align-items:start;
  padding:8px 9px; border-radius:9px; color:inherit;
}
a.rank__row[href]:hover { background:var(--surface-2); text-decoration:none; }
.rank__n {
  font-size:10.5px; font-weight:700; color:var(--text-3); background:var(--surface-2);
  border:1px solid var(--line); border-radius:6px; text-align:center; line-height:18px;
  font-variant-numeric:tabular-nums;
}
.rank__row:first-child .rank__n { background:var(--brand-100); border-color:var(--brand-500); color:var(--brand-800); }
.rank__head { display:flex; justify-content:space-between; gap:10px; font-size:12.5px; align-items:baseline; }
.rank__name { font-weight:650; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rank__val { font-weight:700; white-space:nowrap; font-variant-numeric:tabular-nums; }
.rank__meta {
  display:flex; justify-content:space-between; gap:10px;
  font-size:11px; color:var(--text-3); margin-top:4px;
}
.rank .bar { margin-top:5px; }

/* ---------------- a row that is late ----------------
   In a table sorted so the late jobs come first, the badge in the last-but-one
   column is the only thing saying so — and it is the column furthest from the
   asset name the eye starts on. A red edge carries it back to the left. */
.tbl tbody tr.is-late > td:first-child { box-shadow:inset 3px 0 0 var(--bad); }
.tbl tbody tr.is-today > td:first-child { box-shadow:inset 3px 0 0 var(--warn); }
/* The same bar, named for the state rather than the date, for rows that are
   wrong or worrying without being late: a handset holding records nobody can
   file, a phone that has held work past the chasing hour. */
.tbl tbody tr.is-bad > td:first-child { box-shadow:inset 3px 0 0 var(--bad); }
.tbl tbody tr.is-warn > td:first-child { box-shadow:inset 3px 0 0 var(--warn); }

/* A cell with a name on the first line and its facts under it — the shape of
   the first column on most list pages — and the two plain tones of text.
   These names were in use in a template before any rule matched them. */
.cell-title { font-weight:650; line-height:1.35; }
.cell-meta { font-size:11.5px; color:var(--text-3); margin-top:2px; line-height:1.4; }
.text-bad { color:#b91c1c; }
.muted { color:var(--text-3); }

/* ---------------- where the pickup happened ----------------
   The coordinate pair is a reading the page takes, not a question it asks, so
   it reads as a status line: a dot carrying the state, the fix in plain digits,
   and the boxes only for the times the reading has to be corrected by hand. */
.geo {
  display:grid; gap:10px; padding:12px; border-radius:var(--radius-sm);
  border:1px solid var(--line); background:var(--surface-2);
}
.geo__status { display:flex; align-items:flex-start; gap:8px; font-size:12.5px; color:var(--text-2); line-height:1.45; }
.geo__dot {
  flex:0 0 auto; width:9px; height:9px; margin-top:5px; border-radius:50%;
  background:var(--text-3); box-shadow:0 0 0 3px color-mix(in srgb, var(--text-3) 18%, transparent);
}
.geo[data-state="asking"] .geo__dot { background:var(--info); box-shadow:0 0 0 3px color-mix(in srgb, var(--info) 22%, transparent); animation:geo-pulse 1.1s ease-in-out infinite; }
.geo[data-state="found"]  .geo__dot { background:var(--ok);   box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
.geo[data-state="off"]    .geo__dot { background:var(--warn); box-shadow:0 0 0 3px color-mix(in srgb, var(--warn) 22%, transparent); }
@keyframes geo-pulse { 50% { opacity:.35; } }
@media (prefers-reduced-motion:reduce) { .geo[data-state="asking"] .geo__dot { animation:none; } }
/* The numbers themselves, in the one face where a digit cannot be mistaken for
   another — this is the figure somebody checks a pin against. */
.geo__coords {
  font-family:var(--mono); font-size:12.5px; color:var(--text); font-variant-numeric:tabular-nums;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-sm);
  padding:6px 9px; overflow-wrap:anywhere;
}
/* The reading and the things you can do with it on one line, wrapping together
   on a handset. margin-right pushes the buttons away from the figures without
   needing a second container. */
.geo__actions { align-items:center; }
.geo__actions .geo__coords { margin-inline-end:auto; }
.geo__fields { display:grid; gap:10px; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); }

/* ---------------- what the list is narrowed to ----------------
   A register with a filter on looked exactly like one without: the same table,
   fewer rows, and the reason held only in the state of controls that are folded
   away on most screens — or never touched, because the filter came from a KPI
   tile or a pasted link. This row says it in words, directly above the results,
   and each chip is also the way out of that one filter. */
.applied {
  display:flex; flex-wrap:wrap; align-items:center; gap:7px;
  padding:11px 16px; border-bottom:1px solid var(--line); background:var(--surface-2);
}
.applied__lead {
  font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.05em;
  color:var(--text-3);
}
/* The chip carries the question as well as the answer — "Ward: 3" on its own
   is only readable if you already know which dropdown it came from. */
.applied__chip {
  display:inline-flex; align-items:center; gap:6px; max-width:100%;
  padding:3px 5px 3px 9px; border-radius:999px; font-size:12px; line-height:1.6;
  background:var(--brand-50); border:1px solid var(--brand-100); color:var(--brand-800);
  transition:background .12s, border-color .12s;
}
.applied__chip:hover { background:var(--brand-100); border-color:var(--brand-500); text-decoration:none; }
.applied__k { color:var(--text-2); font-weight:600; }
.applied__v { font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The × is the affordance, so it has to look pressable rather than like the
   last character of the value. */
.applied__x {
  display:grid; place-items:center; width:17px; height:17px; border-radius:999px;
  background:color-mix(in srgb, var(--brand-500) 22%, transparent);
  color:var(--brand-800); font-size:12px; line-height:1; flex:0 0 auto;
}
.applied__chip:hover .applied__x { background:var(--brand-600); color:#fff; }
.applied__clear { font-size:12px; color:var(--text-2); margin-inline-start:4px; }
.applied__clear:hover { color:var(--bad); }
@media (prefers-reduced-motion:reduce) { .applied__chip { transition:none; } }

/* ---------------- a cell that is only a status badge ----------------
   .badge clips its own text with an ellipsis so that a long Bengali label in a
   crowded cell cannot widen the whole column. That is right where the badge
   shares a cell with other content and wrong where the badge *is* the cell:
   the maintenance register's status column sized itself to "Overdue" and then
   quietly cut "Scheduled" to "Schedule" and "Completed" to "Complete" — two
   words that still read as words, so nothing looked broken. Here the badge
   sets the column width instead of being cut to fit it. */
.tbl th.tbl__status, .tbl td.tbl__status { width:1%; white-space:nowrap; }
.tbl td.tbl__status .badge { max-width:none; }

/* ---------------- the permission grid ----------------
   /admin/roles/:id and /admin/users/:id/permissions. Eighty-six checkboxes is a
   lot of page, and what makes it readable is not making it pretty — it is
   making a row scannable left to right, a column top to bottom, and the whole
   thing narrowable to the handful of lines somebody actually came for. So: the
   page name is the only prose in a row, the ticks sit in fixed narrow columns
   under their verb, the groups are the menu's own groups, and a toolbar above
   it all answers "where is X" and "what can this person not do". */

/* ---- toolbar ---- */
.perm-toolbar {
  position:sticky; top:58px; z-index:5;
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:11px 16px; background:var(--surface); border-bottom:1px solid var(--line);
}
.perm-toolbar__acts { display:flex; gap:4px; margin-inline-start:auto; }

.perm-search { position:relative; display:flex; align-items:center; flex:1 1 220px; max-width:320px; }
.perm-search__icon {
  position:absolute; inset-inline-start:10px; display:flex; color:var(--text-3); pointer-events:none;
}
.perm-search input {
  width:100%; padding-inline:32px; height:34px;
  border:1px solid var(--line); border-radius:999px; background:var(--surface-2);
  font:inherit; font-size:13px; color:var(--text);
}
.perm-search input:focus { outline:none; border-color:var(--brand-500); background:var(--surface); }
/* The browser's own clear affordance on a search input is inconsistent between
   engines and invisible on two of them, so the control is drawn here. */
.perm-search input::-webkit-search-cancel-button { display:none; }
.perm-search__clear {
  position:absolute; inset-inline-end:8px; border:0; background:none; cursor:pointer;
  color:var(--text-3); font-size:12px; line-height:1; padding:4px;
}
.perm-search__clear:hover { color:var(--bad); }

.perm-pills { display:flex; gap:4px; flex-wrap:wrap; }
.perm-pill {
  display:inline-flex; align-items:center; gap:6px;
  font:inherit; font-size:12px; font-weight:600; line-height:1;
  padding:8px 11px; border:1px solid var(--line); border-radius:999px;
  background:var(--surface); color:var(--text-2); cursor:pointer; white-space:nowrap;
}
.perm-pill:hover { border-color:var(--brand-500); color:var(--brand-700); }
.perm-pill.is-active { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
.perm-pill__n {
  font-variant-numeric:tabular-nums; font-size:11px;
  padding:1px 6px; border-radius:999px; background:var(--muted-soft); color:var(--text-2);
}
.perm-pill.is-active .perm-pill__n { background:rgb(255 255 255 / .22); color:#fff; }
.perm-pill--diff.is-active { background:var(--warn); border-color:var(--warn); }

.perm-empty { padding:36px 16px; text-align:center; color:var(--text-3); font-size:13px; margin:0; }

/* ---- groups ---- */
.perm-group + .perm-group { border-top:1px solid var(--line); }
.perm-group__head {
  display:flex; align-items:center; gap:9px; flex-wrap:wrap; cursor:pointer;
  padding:10px 16px; background:var(--surface-2); list-style:none;
  position:sticky; top:103px; z-index:4;
}
.perm-group__head::-webkit-details-marker { display:none; }
.perm-group__head:hover { background:var(--brand-50); }
.perm-group__name { font-weight:700; font-size:12px; letter-spacing:.04em;
  text-transform:uppercase; color:var(--text-2); }
.perm-group__count {
  margin-inline-start:auto; font-size:11.5px; color:var(--text-3);
  font-variant-numeric:tabular-nums;
}
.perm-chev { display:flex; color:var(--text-3); transition:transform .15s ease; }
.perm-group[open] > .perm-group__head .perm-chev { transform:rotate(90deg); }
[dir="rtl"] .perm-chev { transform:rotate(180deg); }
[dir="rtl"] .perm-group[open] > .perm-group__head .perm-chev { transform:rotate(90deg); }

/* ---- the table ---- */
.perm-tbl th, .perm-tbl td { padding-top:6px; padding-bottom:6px; }
.perm-tbl__act { width:1%; text-align:center; white-space:nowrap; }
.perm-tbl th.perm-tbl__act { font-size:11px; padding:0; }
.perm-tbl tbody tr:hover { background:var(--brand-50); }
/* A section heading inside a permission group: a band across the table naming
   the run of pages under it, with its own count and its own All. Quieter than
   the group's summary above it — it divides a table, it does not head one — and
   it is not a row, so it does not take the row hover. */
.perm-tbl tr.perm-sec:hover { background:transparent; }
.perm-tbl tr.perm-sec > .perm-sec__cell {
  background:var(--surface-2); border-top:1px solid var(--line);
  padding:6px 10px; text-align:start; font-weight:400;
}
.perm-tbl tbody:first-of-type tr.perm-sec > .perm-sec__cell { border-top:0; }
.perm-sec__name {
  font-size:11px; font-weight:700; letter-spacing:.05em;
  text-transform:uppercase; color:var(--text-2);
}
.perm-sec__count { font-size:11px; color:var(--text-3); margin-inline-start:8px; }
.perm-sec__all { margin-inline-start:8px; }
/* Same reasoning as .def-row: the toolbar and the column headings are both
   pinned, and a row scrolled to lands behind them without this. */
.perm-tbl tbody tr { scroll-margin-top:120px; }

/* A column heading that is also the column's toggle. It has to look pressable
   without shouting, because it is still first of all a heading. */
.perm-col {
  font:inherit; font-size:11px; font-weight:700; letter-spacing:.03em; text-transform:uppercase;
  color:var(--text-2); background:none; border:0; cursor:pointer;
  padding:10px 12px; width:100%; border-radius:var(--radius-sm);
}
.perm-col:hover { background:var(--brand-100); color:var(--brand-800); }

.perm-page { display:flex; align-items:center; gap:9px; min-width:210px; }
.perm-page__icon { color:var(--text-3); display:flex; flex:0 0 auto; }
.perm-page__text { min-width:0; }
.perm-page__name { font-weight:600; font-size:13.5px; }
.perm-page__href { font-size:11px; color:var(--text-3); margin-inline-start:6px; }
/* The row's own toggles, which appear when the row is under the pointer or
   holds focus — a permanent pair of buttons on twenty-six rows is fifty-two
   buttons competing with the ticks that matter. */
.perm-page__acts { display:flex; gap:3px; margin-inline-start:auto; opacity:0; transition:opacity .12s ease; }
.perm-tbl tbody tr:hover .perm-page__acts,
.perm-tbl tbody tr:focus-within .perm-page__acts,
.perm-tbl tbody tr[data-perm-differs] .perm-page__acts { opacity:1; }
.perm-mini {
  font:inherit; font-size:10.5px; font-weight:700; line-height:1; cursor:pointer;
  padding:4px 7px; border:1px solid var(--line); border-radius:999px;
  background:var(--surface); color:var(--text-3);
}
.perm-mini:hover { border-color:var(--brand-500); color:var(--brand-700); background:var(--brand-50); }
.perm-mini--undo { color:var(--warn); border-color:color-mix(in srgb, var(--warn) 40%, var(--line)); }
.perm-mini--undo:hover { background:var(--warn-soft); color:#92400e; border-color:var(--warn); }

/* The whole cell is the target, not the 16px box inside it — this table is used
   on a laptop trackpad by somebody ticking thirty boxes in a row. */
.perm-cell { display:grid; place-items:center; padding:4px 10px; cursor:pointer; margin:0; }
.perm-cell input { width:16px; height:16px; accent-color:var(--brand-600); cursor:pointer; }
.perm-na { color:var(--line); user-select:none; }
.check--inline { display:inline-flex; margin-inline-end:12px; white-space:nowrap; border-radius:999px; padding:2px 6px; }

/* ---- an exception, drawn as one ----
   On the per-account screen a tick can mean two different things — "the role
   says so" and "somebody decided this about this person" — and a page that
   draws them identically is a page that cannot answer its own question. Added
   is a green ring, taken away is a red one with the ghost of the tick that
   would otherwise be there. */
.perm-tbl__act.is-added, .perm-tbl__act.is-removed { position:relative; }
.perm-tbl__act.is-added .perm-cell { background:var(--ok-soft); border-radius:999px; }
.perm-tbl__act.is-removed .perm-cell { background:var(--bad-soft); border-radius:999px; }
.perm-tbl__act.is-added .perm-cell input { accent-color:var(--ok); }
.perm-tbl__act.is-removed .perm-cell::after {
  content:""; position:absolute; inset-inline-start:50%; top:50%;
  width:15px; height:2px; margin-inline-start:-7px; border-radius:2px;
  background:var(--bad); pointer-events:none;
}
.check--inline.is-added { background:var(--ok-soft); }
.check--inline.is-removed { background:var(--bad-soft); text-decoration:line-through; }
tr[data-perm-differs] > td:first-child { box-shadow:inset 3px 0 0 var(--warn); }
[dir="rtl"] tr[data-perm-differs] > td:first-child { box-shadow:inset -3px 0 0 var(--warn); }

.perm-legend { display:flex; gap:12px; flex-wrap:wrap; margin-inline-start:auto; font-size:11.5px; color:var(--text-3); }
.perm-legend__item { display:inline-flex; align-items:center; gap:5px; }
.perm-legend__dot {
  width:11px; height:11px; border-radius:3px; background:var(--brand-100);
  border:1px solid var(--brand-500); display:inline-block;
}
.perm-legend__dot--add { background:var(--ok-soft); border-color:var(--ok); }
.perm-legend__dot--rm { background:var(--bad-soft); border-color:var(--bad); }

/* ---- the summary card on the per-account screen ---- */
/* `flex-direction:row` is not redundant: .card is a flex *column*, and without
   overriding it the `flex:1 1 260px` below is read as a 260px-tall basis on
   the main axis — which is how this card spent a release being a quarter of a
   screen of white space with one sentence at the top of it. */
.perm-summary {
  display:flex; flex-direction:row; align-items:center;
  gap:13px; padding:14px 16px; flex-wrap:wrap;
}
.perm-summary__icon { display:flex; color:var(--warn); flex:0 0 auto; }
.perm-summary.is-clean .perm-summary__icon { color:var(--ok); }
.perm-summary__text { flex:1 1 260px; min-width:0; }
.perm-summary__head { font-weight:700; font-size:14.5px; }
.perm-summary__sub { font-size:12.5px; color:var(--text-2); margin-top:3px; line-height:1.5; }
.perm-summary__acts { flex:0 0 auto; }

/* ---- how much of the system a role can reach ----
   A proportion is the only form of "62" that means anything without the
   catalogue open beside it. */
.perm-meter { height:5px; border-radius:999px; background:var(--muted-soft); overflow:hidden; }
.perm-meter > span { display:block; height:100%; background:var(--brand-500); border-radius:999px; }

/* The grid is longer than a screen, and a Save button underneath six tables is
   a Save button nobody finds. */
.perm-save {
  position:sticky; bottom:0; z-index:6; margin-top:14px;
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:11px 16px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); box-shadow:0 -4px 14px rgb(15 23 42 / .07);
  padding-bottom:calc(11px + env(safe-area-inset-bottom, 0px));
}
.perm-save__count { font-size:13px; font-weight:650; color:var(--text-2); }
.perm-save__diff {
  font-size:12px; font-weight:700; padding:3px 9px; border-radius:999px;
  background:var(--warn-soft); color:#92400e;
}
.perm-save .btn-group { margin-inline-start:auto; }

@media (max-width:820px) {
  /* Below the breakpoint the topbar stops being the only thing pinned, and a
     toolbar and a group header stacked under it eat half a short screen. */
  .perm-toolbar, .perm-group__head { position:static; }
}
@media (max-width:640px) {
  /* The verb columns stay; the href does not. On a phone the page name and its
     ticks are the whole point and the URL underneath is what pushes them off
     the right edge. */
  .perm-page__href { display:none; }
  .perm-page__acts { opacity:1; }
  .perm-search { max-width:none; }
  .perm-toolbar__acts { display:none; }
  .perm-legend { display:none; }
}

.field__hint { font-size:11.5px; color:var(--text-3); margin-top:4px; line-height:1.45; }
.chip--muted { opacity:.55; }

@media (max-width:640px) {
  /* The verb columns stay; the href does not. On a phone the page name and five
     ticks are the whole point and the URL underneath it is what pushes them
     off the right edge. */
  .perm-page__href { display:none; }
  .perm-group__bulk { width:100%; margin-inline-start:0; }
}

/* ==== system modules: what this deployment uses at all ====================
   Not the permission grid. That grid is a matrix because it answers the same
   four questions about twenty-seven pages, twenty-odd times over. This screen
   is answered once, by one person, about the system itself — "is there a
   handset fleet here" — so a page is a line with a switch on it, and the verbs
   are small print under the line for the one case in ten where the answer is
   "yes, but not deletions". */

.def-summary {
  display:flex; flex-direction:row; align-items:center;
  gap:13px; padding:14px 16px; flex-wrap:wrap;
}
.def-summary__icon { display:flex; color:var(--brand-600); flex:0 0 auto; }
.def-summary__text { flex:1 1 260px; min-width:0; }
.def-summary__head { font-weight:700; font-size:14.5px; }
.def-summary__sub { font-size:12.5px; color:var(--text-2); margin-top:3px; line-height:1.5; }
.def-summary__acts { flex:0 0 auto; margin-inline-start:auto; }

.def-toolbar {
  position:sticky; top:58px; z-index:5;
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  margin:12px 0; padding:10px 12px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  box-shadow:0 2px 10px rgb(15 23 42 / .05);
}

.def-list { list-style:none; margin:0; padding:0; }
.def-row {
  display:grid; grid-template-columns:auto 1fr auto; align-items:start;
  gap:12px; padding:11px 16px;
  /* The toolbar above is pinned, so a row scrolled to by the browser — a
     find-in-page hit, a focused switch — would otherwise arrive underneath it. */
  scroll-margin-top:120px;
}
.def-row + .def-row { border-top:1px solid var(--line); }
.def-row:hover { background:var(--brand-50); }
/* A page switched off is dimmed rather than removed: the row is still the
   thing you click to bring it back, and a list that reflows as you switch
   things off is a list you lose your place in. */
.def-row.is-off .def-row__main { opacity:.5; }
.def-row.is-locked { background:var(--surface-2); }
/* The same heading on the module switches, where the list is <li>s rather than
   table rows. Its All sits at the far end, because it acts on the whole run
   under it and the switches it moves are down the left. */
.def-sec {
  display:flex; align-items:center; gap:8px; padding:7px 12px;
  background:var(--surface-2); border-top:1px solid var(--line);
}
.def-list > .def-sec:first-child { border-top:0; }
.def-sec__name {
  font-size:11px; font-weight:700; letter-spacing:.05em;
  text-transform:uppercase; color:var(--text-2);
}
.def-sec__count { font-size:11px; color:var(--text-3); }
.def-sec__all { margin-inline-start:auto; }
/* Bangla has no upper case, and the tracking that makes a Latin small-caps
   heading readable only pulls the conjuncts apart. */
.lang-bn .perm-sec__name, .lang-bn .def-sec__name {
  text-transform:none; letter-spacing:0; font-size:12.5px;
}

.def-row__main { min-width:0; }
.def-row__name {
  display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  font-weight:650; font-size:13.5px;
}
.def-row__acts {
  display:flex; align-items:center; gap:10px 14px; flex-wrap:wrap; margin-top:6px;
}
.def-row__none { font-size:11.5px; color:var(--text-3); }
.def-row__side {
  display:flex; align-items:center; gap:8px; flex:0 0 auto; padding-top:2px;
}
.def-row__users {
  display:inline-flex; align-items:center; gap:4px;
  font-size:11.5px; font-weight:650; color:var(--text-3);
}

/* ---- the switch ----
   A checkbox says "tick this one too"; a switch says "this is on or off", which
   is the sentence this page is made of. Same input underneath either way.

   Scoped to the markup that actually paints a track, because `.switch` is older
   than this block and still means a plain checkbox with a label beside it in
   five places — "remember me", the automatic backup, the field app's GPS, a
   notification template's active flag, and the days a route plan runs. Those
   are all "tick this one too" and are right to be checkboxes. Unscoped, the
   rules below hid the box in every one of them behind `opacity:0` with nothing
   drawn in its place: a row of words that quietly changed state when pressed
   and never showed which were on. The track is what says "this one is a
   toggle", so it is what selects for one. */
.switch:has(> .switch__track) {
  position:relative; display:inline-flex; flex:0 0 auto; cursor:pointer; padding-top:1px;
}
.switch input:has(+ .switch__track) {
  /* Above the track it draws, not under it: the input is the hit area, and a
     click that lands on the painted dot instead of on it is a switch that
     ignores every other press. */
  position:absolute; inset:0; z-index:1;
  width:100%; height:100%; margin:0; opacity:0; cursor:pointer;
}
.switch__track {
  display:block; width:38px; height:22px; border-radius:999px;
  background:var(--muted-soft); border:1px solid var(--line);
  transition:background .15s ease, border-color .15s ease;
}
.switch__dot {
  display:block; width:16px; height:16px; margin:2px; border-radius:50%;
  background:var(--surface); box-shadow:0 1px 3px rgb(15 23 42 / .28);
  transition:transform .15s ease;
}
.switch input:checked + .switch__track { background:var(--brand-600); border-color:var(--brand-600); }
.switch input:checked + .switch__track .switch__dot { transform:translateX(16px); }
[dir="rtl"] .switch input:checked + .switch__track .switch__dot { transform:translateX(-16px); }
.switch input:focus-visible + .switch__track { outline:2px solid var(--brand-500); outline-offset:2px; }
.switch--fixed, .switch--fixed input { cursor:default; }
.switch--fixed .switch__track { opacity:.55; }

/* Why a grid is shorter than the catalogue. Without this line a page switched
   off system-wide simply is not there, and whoever is looking for it has
   nowhere to find out why. */
.perm-off-note {
  display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  margin:0; padding:10px 16px; border-top:1px solid var(--line);
  font-size:12px; color:var(--text-3); background:var(--surface-2);
}
.perm-off-note span { display:flex; }

@media (max-width:820px) { .def-toolbar { position:static; } }
@media (max-width:640px) {
  .def-row { grid-template-columns:auto 1fr; }
  /* The count of who holds it moves under the name rather than squeezing the
     name into two words. */
  .def-row__side { grid-column:2; padding-top:0; }
}

/* ---- what is different, as a list you can act on ----
   The Exceptions filter has always been able to show these, but it shows them
   as a grid with the ringed boxes somewhere in it. Four exceptions are four
   short sentences; each one is the button that undoes it. */
.perm-diffs { display:flex; flex-wrap:wrap; gap:6px; margin-top:9px; }
.perm-diff {
  display:inline-flex; align-items:center; gap:6px;
  padding:3px 8px 3px 7px; border-radius:999px; cursor:pointer;
  font:inherit; font-size:11.5px; font-weight:650; line-height:1.5;
  border:1px solid var(--line); background:var(--surface-2); color:var(--text-2);
}
.perm-diff:hover { border-color:var(--text-3); }
.perm-diff.is-added { background:var(--ok-soft); border-color:color-mix(in srgb, var(--ok) 40%, var(--line)); }
.perm-diff.is-removed { background:var(--bad-soft); border-color:color-mix(in srgb, var(--bad) 40%, var(--line)); }
.perm-diff__sign { font-weight:800; }
.perm-diff.is-added .perm-diff__sign { color:var(--ok); }
.perm-diff.is-removed .perm-diff__sign { color:var(--bad); }
.perm-diff__x { color:var(--text-3); font-size:10px; }
.perm-diff:hover .perm-diff__x { color:var(--text-1); }

/* No sticky column headings here, though the grid is long enough to want them:
   the table sits inside .table-wrap, which is a scroll container, so a sticky
   thead is offset from *that* box rather than from the viewport — on a group
   with one row in it the heading came to rest on top of its own row and ate
   every click aimed at it. The group headings, which are outside the wrapper,
   carry the same job. */
.perm-toolbar__gap { flex:0 0 8px; }

/* ==== form rules: what the forms ask for =================================
   A rule is a row and its options are a segmented control, each option saying
   underneath what choosing it will mean. The consequence rather than the value
   is the point: "Required" is a word about the box, "The form will not send
   without one" is what the person deciding needs. */

.fr-group__icon { display:flex; color:var(--brand-600); flex:0 0 auto; }

.fr-list { list-style:none; margin:0; padding:0; }
.fr-rule { padding:15px 16px; }
.fr-rule + .fr-rule { border-top:1px solid var(--line); }
/* A rule moved but not yet saved says so, so the save button is not the only
   thing that knows. */
.fr-rule.is-changed { background:color-mix(in srgb, var(--warn-soft) 40%, transparent); }

.fr-rule__head {
  display:flex; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-bottom:11px;
}
.fr-rule__text { flex:1 1 320px; min-width:0; }
.fr-rule__name { font-weight:700; font-size:14px; }
.fr-rule__hint {
  margin:4px 0 0; font-size:12.5px; line-height:1.55; color:var(--text-2); max-width:68ch;
}
.fr-rule__where {
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  flex:0 0 auto; font-size:11px; color:var(--text-3);
}
.fr-rule__where code {
  background:var(--surface-2); border:1px solid var(--line);
  border-radius:5px; padding:1px 6px; font-size:11px;
}

/* The options. A grid rather than a row of pills: each carries a sentence, and
   three sentences side by side need equal columns or the longest one decides
   how the other two wrap. */
.fr-opts { display:grid; gap:8px; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr)); }
.fr-opt {
  position:relative;
  display:flex; align-items:flex-start; gap:9px; cursor:pointer;
  padding:10px 12px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface-2);
}
.fr-opt:hover { border-color:var(--brand-500); }
.fr-opt.is-on { border-color:var(--brand-600); background:var(--brand-50); }
/* The whole card is the hit area, the way .switch does it. A zero-size input
   is focusable and clickable by keyboard but not by a pointer aimed anywhere
   except the label text — and it escapes to the nearest positioned ancestor,
   which was the page. */
.fr-opt input { position:absolute; inset:0; z-index:1; width:100%; height:100%; margin:0; opacity:0; cursor:pointer; }
.fr-opt__mark {
  flex:0 0 auto; width:16px; height:16px; margin-top:1px; border-radius:50%;
  border:2px solid var(--text-3); background:var(--surface);
}
.fr-opt.is-on .fr-opt__mark {
  border-color:var(--brand-600);
  /* The dot is drawn with a ring rather than a child element, so the control
     is one box in the markup and the label stays clickable all over. */
  box-shadow:inset 0 0 0 3px var(--surface), inset 0 0 0 8px var(--brand-600);
}
.fr-opt input:focus-visible + .fr-opt__mark { outline:2px solid var(--brand-500); outline-offset:2px; }
.fr-opt__text { min-width:0; }
.fr-opt__name { display:block; font-weight:650; font-size:12.5px; }
.fr-opt__hint {
  display:block; margin-top:2px; font-size:11.5px; line-height:1.5; color:var(--text-2);
}
.fr-opt--fixed, .fr-opt--fixed input { cursor:default; }

/* A picker with one source says which, where the switch would have been. */
.pp-only {
  display:flex; align-items:center; gap:6px; margin:0 0 9px;
  font-size:12px; color:var(--text-3);
}
.pp-only svg { flex:0 0 auto; }

/* The citizens' address, on /admin/forms. A box rather than a segmented
   control, because it is a word the municipality chooses rather than a choice
   between options — and shown as the whole address a citizen would type, since
   a path fragment on its own is not the thing being decided. */
.fr-path { max-width:560px; }
.fr-path__box {
  display:flex; align-items:center; gap:0;
  border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface-2); overflow:hidden;
}
.fr-path__box:focus-within { border-color:var(--brand-600); box-shadow:0 0 0 3px var(--brand-50); }
.fr-path__origin {
  padding:9px 0 9px 12px; font-size:12.5px; color:var(--text-3); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; max-width:52%;
}
.fr-path__slash { padding:9px 1px; font-size:13px; color:var(--text-3); }
.fr-path input {
  flex:1 1 auto; min-width:0; border:0; background:transparent;
  padding:9px 12px 9px 0; font:inherit; font-size:14px; font-weight:650;
}
.fr-path input:focus { outline:none; }
.fr-path__note { margin:7px 0 0; font-size:11.5px; color:var(--text-3); line-height:1.5; }
/* The answer to "will the codes we already printed stop working". */
.fr-path__kept {
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  margin:8px 0 0; font-size:11.5px; color:var(--text-2);
}
.fr-path__kept span { display:flex; color:var(--ok); }
.fr-path__kept code {
  background:var(--ok-soft); border:1px solid color-mix(in srgb, var(--ok) 30%, var(--line));
  border-radius:5px; padding:1px 6px; font-size:11px;
}

/* ==== the citizens' landing page ==========================================
   Same shell as the form (.report) — one green header, one column, phone
   width — because it is the same place to whoever is holding the phone. What
   differs is the order: the button first, then what the municipality has been
   doing, then how it works, then who to ring. */

.land__notice {
  display:flex; gap:9px; align-items:flex-start;
  padding:11px 13px; margin:16px 0; border-radius:var(--radius);
  background:var(--brand-50); border:1px solid color-mix(in srgb, var(--brand-600) 25%, var(--line));
}
.land__notice span { display:flex; color:var(--brand-700); flex:0 0 auto; margin-top:1px; }
.land__notice p { margin:0; font-size:13.5px; line-height:1.55; }

/* The one thing the page exists for. Full width and tall enough to be the
   obvious target on a phone held one-handed. */
.land__go {
  display:flex; align-items:center; justify-content:center; gap:9px;
  width:100%; padding:15px 18px; font-size:16px; font-weight:700; margin-top:18px;
}
.land__go-note { margin:8px 0 0; font-size:12px; color:var(--text-3); text-align:center; }

.land__sec { margin-top:28px; }
.land__h { font-size:15px; font-weight:700; margin:0 0 4px; }
.land__sub { margin:0 0 12px; font-size:12.5px; color:var(--text-2); }

/* ---- announcements ---- */
.land__news { list-style:none; margin:12px 0 0; padding:0; display:grid; gap:12px; }
.land__post {
  display:flex; gap:12px; align-items:flex-start;
  border:1px solid var(--line); border-radius:var(--radius); overflow:hidden;
  background:var(--surface);
}
.land__post.is-pinned { border-color:color-mix(in srgb, var(--brand-600) 40%, var(--line)); }
.land__post-img {
  flex:0 0 96px; width:96px; height:96px; object-fit:cover; display:block;
}
.land__post-text { padding:11px 13px 12px; min-width:0; }
.land__post-meta {
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  font-size:11px; color:var(--text-3);
}
.land__pin { display:inline-flex; align-items:center; gap:3px; color:var(--brand-700); font-weight:650; }
.land__post-t { margin:3px 0 0; font-size:14px; font-weight:700; line-height:1.4; }
.land__post-b {
  margin:5px 0 0; font-size:13px; line-height:1.6; color:var(--text-2);
  /* Three lines on the page; the whole thing would turn a landing page into a
     newsletter nobody scrolls past. */
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
}

/* ---- how it works ---- */
.land__steps { list-style:none; margin:12px 0 0; padding:0; display:grid; gap:11px; }
.land__step { display:flex; gap:11px; align-items:flex-start; }
.land__step-n {
  flex:0 0 auto; width:24px; height:24px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:var(--brand-600); color:#fff; font-size:12px; font-weight:700;
}
.land__step-ic { display:flex; color:var(--brand-700); flex:0 0 auto; margin-top:3px; }
.land__step-t { font-size:13.5px; font-weight:650; }
.land__step-p { margin:2px 0 0; font-size:12.5px; line-height:1.55; color:var(--text-2); }

/* ---- the municipality-wide code ---- */
/* The address under the square. `anywhere` rather than `break-all`: the latter
   splits at whatever character reaches the edge, so "teknaf.gov.bd/tellus" came
   apart mid-word while there was still room to wrap it whole. */
.land__qr-url {
  display:inline-block; max-width:100%; font-size:11.5px;
  overflow-wrap:anywhere; word-break:normal;
  background:var(--surface-2); border:1px solid var(--line); border-radius:5px; padding:3px 7px;
}

/* ---- who to contact ---- */
.land__contacts { list-style:none; margin:0; padding:0; display:grid; gap:9px; }
.land__contact {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:11px 13px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
.land__contact-main { flex:1 1 200px; min-width:0; }
.land__contact-name { font-size:14px; font-weight:700; }
.land__contact-title { font-size:12.5px; color:var(--text-2); margin-top:2px; }
.land__contact-hours { font-size:11.5px; color:var(--text-3); margin-top:3px; }
/* A telephone number on a page held in a hand is a button, not a string. */
.land__call { flex:0 0 auto; display:inline-flex; align-items:center; gap:6px; }

.land__office { font-size:13px; }
.land__office-line { display:flex; align-items:center; gap:8px; margin:6px 0 0; }
.land__office-line span { display:flex; color:var(--text-3); flex:0 0 auto; }
.land__privacy { margin:26px 0 0; font-size:11.5px; line-height:1.6; color:var(--text-3); }
.land__bare { margin:20px 0 0; font-size:12.5px; color:var(--text-3); text-align:center; }

@media (max-width:420px) {
  /* The picture stops being a thumbnail beside the text and becomes the top of
     the card: at this width 96px of image leaves the headline three words wide. */
  .land__post { flex-direction:column; }
  .land__post-img { width:100%; height:150px; flex:0 0 auto; }
}

/* ==== the editor behind it ================================================ */
/* Both languages side by side, so neither is the one somebody forgets. */
.land-ed__pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.land-ed__pair textarea, .land-ed__pair input { width:100%; }
.land-ed__toggles { display:flex; gap:14px; flex-wrap:wrap; }
/* A telephone number is not a paragraph; a box the width of the card invites
   somebody to paste a sentence into it. */
.land-ed__short input { max-width:260px; }

/* ---- the page's own words ----
 *
 * One rhythm, one label, one hint, across four cards whose fields are of three
 * different kinds. Before this the spacing was owned by whichever field
 * happened to be next: two bilingual fields had 16px between them, a bilingual
 * field after a plain one had nothing at all, and the address boxes ran into
 * the label of the field below them. Owning it here — the card's body spaces
 * its own children — means a field added to any of these cards is spaced
 * correctly by existing, which is the only version of this that survives the
 * next field somebody adds.
 */
.lcard { margin-bottom:14px; }
.lcard .card__body > * + * { margin-top:18px; }
/* The same figure wherever these two set their own spacing, so a stack of
   bilingual fields and a stack of mixed ones read alike. */
.bifield + .bifield { margin-top:18px; }
.lpair { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:18px; }

/* Every label on this form, whatever kind of field it belongs to. A plain
   field's label is the application's 12.5px/600 and a bilingual field's was
   13px/650 — two ranks of label for one rank of thing, side by side in the
   same card ("Google Maps address" light, "A line under the map" dark). The
   stronger of the two wins here, and only here: `.field label` is on every
   form in the application and is not this screen's to change. */
/* `.field__label` is the same thing for a caption that names no control — the
   map preview, the film that is already uploaded — where a <label> would point
   at nothing. */
.lcard .field > label, .lcard .field > .field__label, .lcard .bifield__label {
  display:block; font-size:13px; font-weight:650; color:var(--text-1); margin-bottom:0;
}
/* A hint belongs to the label above it, not to the box below. `.field` spaces
   its children 5px and `.field__hint` added 4 more on top — so the sentence
   explaining a field sat further from its own label than from the input, and
   read as a caption for the wrong thing. */
.lcard .field > .field__hint { margin-top:0; margin-bottom:8px; }

/* The language of each box, and it stays: a placeholder is gone as soon as
   there is anything in the box, which is exactly when you need to know which
   one you are looking at. */
.bifield__pair { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.bifield__box { min-width:0; display:flex; flex-direction:column; gap:4px; }
/* Written as a child of the box rather than on its own, because it is a
   <label> inside a .field and `.field label` — the application's own rule for
   every form — is the more specific of the two. So this was drawn at 12.5px in
   the label colour: the same weight as the name of the field above it, which
   is the one thing a marker saying "this box is the Bangla one" must not be. */
.bifield__box > .bifield__lang {
  font-size:11px; font-weight:600; color:var(--text-3);
  text-transform:uppercase; letter-spacing:.04em;
}
.bifield__box input, .bifield__box textarea { width:100%; }
.lcard__off, .lcard__state {
  display:flex; align-items:flex-start; gap:7px; margin:0;
  font-size:12.5px; line-height:1.5; padding:9px 11px; border-radius:8px;
}
.lcard__off { background:var(--warn-soft); color:#92400e; }
/* Inside the field it belongs to, so it moves with it: what an address was
   understood to be is part of that box, not a paragraph between two of them. */
.lcard__state { margin:3px 0 0; padding:0; background:none; }
.lcard__state.is-ok { color:var(--ok); }
.lcard__state.is-bad { color:var(--bad); }
.lform__foot {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  padding:14px 16px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}

@media (max-width:640px) {
  .bifield__pair, .lpair { grid-template-columns:1fr; }
}

/* ---- the links grid, in the editor ----
   The same row-at-a-time shape as the collection times: somebody comes to add
   one link or take one down, not to review six. */
.lgrid__ic { width:44px; text-align:center; color:var(--text-3); }
.lgrid__ic[data-kind="facebook"]  { color:#1877f2; }
.lgrid__ic[data-kind="youtube"]   { color:#ff0033; }
.lgrid__ic[data-kind="instagram"] { color:#c13584; }
.lgrid__ic[data-kind="whatsapp"]  { color:#128c7e; }
.lgrid__ic[data-kind="linkedin"]  { color:#0a66c2; }
.lgrid__ic[data-kind="telegram"]  { color:#229ed9; }
.lgrid__lbl { min-width:170px; display:flex; flex-direction:column; gap:5px; }
.lgrid__url { min-width:240px; }
.lgrid__ord { width:90px; }
.lgrid__open {
  display:inline-flex; align-items:center; gap:4px; margin-top:4px;
  font-size:11.5px; color:var(--text-3); overflow-wrap:anywhere;
}
.lgrid input[type="text"], .lgrid input[type="url"], .lgrid input[type="number"] { width:100%; }
/* The row at the foot where the next one is added: one line on a desktop, a
   stack on anything narrower. */
.lnew__form { display:flex; flex-wrap:wrap; gap:12px; align-items:flex-end; }
.lnew__form .field { flex:1 1 180px; min-width:0; margin:0; }
.lnew__form .lnew__ord { flex:0 0 110px; }
.lnew .field__hint { margin:8px 0 0; }

/* ---- the map, in the editor ----
   Not a facade here. This screen is signed in and its whole purpose is to see
   that the pasted address landed on the right building, which a button you
   have to press first does not help with. Short, because it is a check rather
   than a map to read. */
.mapeek {
  margin-top:6px; border-radius:12px; overflow:hidden;
  border:1px solid var(--line); background:var(--muted-soft); height:220px;
}
.mapeek__frame { width:100%; height:100%; border:0; display:block; }

/* ---- the film, uploaded ---- */
/* Declared outside the form that saves the page, and never drawn. */
.film__form { display:none; }
.film__row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:10px; }
/* The file picker, in the same hand as the boxes above it. Only the button
   inside it is the browser's to draw, and ::file-selector-button is where it
   is told what this application's buttons look like. */
.film__pick {
  flex:1 1 220px; min-width:0; font-size:13px; color:var(--text-2);
  padding:6px 10px; border:1px solid var(--line-strong); border-radius:var(--radius-sm);
  background:var(--surface);
}
.film__pick::file-selector-button {
  font:inherit; font-weight:650; color:var(--text-1); cursor:pointer;
  margin-inline-end:10px; padding:5px 12px; border-radius:999px;
  border:1px solid var(--line-strong); background:var(--muted-soft);
}
.film__pick::file-selector-button:hover { background:var(--brand-50); border-color:var(--brand-600); color:var(--brand-700); }
/* Small on purpose: it is a check that the right file arrived, not a player. */
.film__peek { width:180px; max-width:100%; border-radius:8px; border:1px solid var(--line); background:#000; }
.film__state { margin:8px 0 0; font-size:12.5px; color:var(--text-2); }
.film__state.is-bad { color:var(--bad); }
.film__bar {
  margin-top:8px; height:6px; border-radius:999px; overflow:hidden;
  background:var(--muted-soft);
}
.film__bar > span { display:block; height:100%; width:0; background:var(--brand-600); transition:width .15s linear; }

/* ---- the collection-times grid, a row at a time ---- */
/* Declared before the table and never seen: each holds the marker that says its
   row posted. display:none does not stop a form submitting. */
.wgrid__form { display:none; }
.wgrid__save { white-space:nowrap; text-align:end; }
/* A touched row, until its own Save is pressed. */
.wgrid tr.is-dirty > td { background:var(--warn-soft); }
.wgrid__dirty { margin-inline-start:6px; }

/* ---- the posts panel ---- */
/* The row is a grid, not a flex line: a thumbnail slot that is there whether or
   not there is a picture, the title taking what is left, and the buttons in a
   column of their own. As a flex row the titles stepped in and out by 56px
   depending on whether somebody had attached a photograph, and the delete
   button sat in a different place on every line. */
.lpost { display:grid; grid-template-columns:44px minmax(0,1fr) auto; gap:12px; align-items:center; }
.lpost__thumb { width:44px; height:44px; flex:0 0 auto; }
.lpost__noimg {
  width:44px; height:44px; border-radius:6px; display:grid; place-items:center;
  background:var(--surface-2); color:var(--text-3); border:1px solid var(--line);
}
.lpost__acts { display:flex; align-items:center; gap:6px; flex-wrap:wrap; justify-content:flex-end; }
.lpost__acts form { display:inline; }
/* The pin is an icon button and reads as one: on, it is the brand colour and
   filled; off, it is the same ghost button as everything beside it. */
.lpost__pin.is-on { background:var(--brand-50); color:var(--brand-700); border-color:var(--brand-100); }

.lposts__bar {
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:11px 16px; border-bottom:1px solid var(--line); background:var(--surface-2);
}
.lposts__search { display:flex; align-items:center; gap:6px; flex:1 1 240px; min-width:0; }
.lposts__search input { flex:1 1 auto; min-width:0; }
.lposts__chips { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.lchip {
  display:inline-flex; align-items:center; gap:6px; padding:4px 10px;
  border:1px solid var(--line); border-radius:999px; background:var(--surface);
  font-size:12px; font-weight:600; color:var(--text-2); text-decoration:none;
  white-space:nowrap;
}
.lchip:hover { border-color:var(--brand-500); color:var(--brand-700); }
.lchip.is-on { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
.lchip__n { font-variant-numeric:tabular-nums; opacity:.75; }
.card__head-act { margin-inline-start:auto; }

/* Wide enough for the two-language pairs inside: these editors are a column of
   English-beside-Bangla fields, and at the default width they stack. */
dialog.modal--wide { width:min(760px, calc(100vw - 28px)); }

@media (max-width:640px) {
  .lpost { grid-template-columns:34px minmax(0,1fr); }
  .lpost__thumb, .lpost__noimg, .land-ed__thumb { width:34px; height:34px; }
  /* The buttons drop to their own line rather than squeezing the title to
     nothing — five targets on one phone line is five mis-taps. */
  .lpost__acts { grid-column:1 / -1; justify-content:flex-start; }
  .lposts__bar { padding:10px 12px; }
}

.land-ed__list { list-style:none; margin:0; padding:0; }
.land-ed__row {
  display:flex; align-items:center; gap:12px; padding:11px 16px;
}
.land-ed__row + .land-ed__row { border-top:1px solid var(--line); }
.land-ed__row.is-draft { background:var(--surface-2); }
.land-ed__row.is-draft .land-ed__row-t { color:var(--text-2); }
.land-ed__thumb { width:44px; height:44px; object-fit:cover; border-radius:6px; flex:0 0 auto; }
.land-ed__row-main { flex:1 1 auto; min-width:0; }
.land-ed__row-t { font-size:13.5px; font-weight:650; display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
.land-ed__row-s { font-size:11.5px; color:var(--text-3); margin-top:2px; }
.land-ed__editor { border-top:1px solid var(--line); background:var(--surface-2); }
.land-ed__form { padding:14px 16px; }
.card__foot--block { display:block; }

@media (max-width:640px) {
  .land-ed__pair { grid-template-columns:1fr; }
}

/* The staff door, at the bottom of the citizens' page. Deliberately quiet: the
   municipality's own address answers with this page now, so the people who work
   there need a way in — but it is not what anybody else came for. */
.land__staff { margin:18px 0 0; text-align:center; font-size:11.5px; }
.land__staff a { color:var(--text-3); text-decoration:underline; }
.land__staff a:hover { color:var(--brand-700); }

/* ---- when is my waste collected ----
   The commonest question anybody asks about waste collection. A ward is a row,
   because that is the unit somebody knows they live in. */
.land__wards { list-style:none; margin:12px 0 0; padding:0; display:grid; gap:7px; }
.land__ward {
  display:grid; grid-template-columns:1fr auto; gap:2px 12px; align-items:center;
  padding:10px 13px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
.land__ward-name { font-size:13.5px; font-weight:650; min-width:0; }
.land__ward-when { display:flex; align-items:center; gap:8px; flex-wrap:wrap; justify-self:end; }
.land__ward-freq {
  font-size:11px; font-weight:700; padding:2px 8px; border-radius:999px;
  background:var(--brand-50); color:var(--brand-700);
}
.land__ward-freq--none { background:var(--muted-soft); color:var(--text-3); font-weight:600; }
.land__ward-time {
  display:inline-flex; align-items:center; gap:4px;
  font-size:12.5px; font-weight:650; color:var(--text-1); font-variant-numeric:tabular-nums;
}
.land__ward-time svg { color:var(--text-3); }
.land__ward-pts { grid-column:1; font-size:11px; color:var(--text-3); }
.land__note { margin:11px 0 0; font-size:12px; color:var(--text-2); }

/* ---- what goes where ----
   The waste-type register the crews already work to, shown to the people who
   fill the bins. Each carries the colour an administrator gave it. */
.land__types { list-style:none; margin:12px 0 0; padding:0; display:flex; flex-wrap:wrap; gap:7px; }
.land__type {
  display:inline-flex; align-items:center; gap:7px;
  padding:7px 11px 7px 9px; border-radius:999px;
  border:1px solid color-mix(in srgb, var(--wt) 35%, var(--line));
  background:color-mix(in srgb, var(--wt) 8%, var(--surface));
}
.land__type-ic { display:flex; color:var(--wt); flex:0 0 auto; }
.land__type-name { font-size:12.5px; font-weight:650; }
.land__type-tag {
  font-size:10px; font-weight:700; letter-spacing:.02em; text-transform:uppercase;
  padding:1px 6px; border-radius:999px; background:var(--ok-soft); color:var(--ok);
}

/* ---- the service in numbers ----
   Published because a municipality that shows its work is answerable for it. */
.land__figs {
  list-style:none; margin:12px 0 0; padding:0;
  display:grid; grid-template-columns:repeat(2, 1fr); gap:9px;
}
.land__fig {
  padding:13px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); text-align:center;
}
.land__fig--good { border-color:color-mix(in srgb, var(--ok) 40%, var(--line)); background:var(--ok-soft); }
.land__fig-n {
  display:block; font-size:24px; font-weight:800; line-height:1.15;
  font-variant-numeric:tabular-nums; color:var(--brand-700);
}
.land__fig--good .land__fig-n { color:var(--ok); }
.land__fig-l { display:block; margin-top:3px; font-size:11.5px; color:var(--text-2); line-height:1.4; }

/* Where it went, as a shape rather than three more numbers. */
.land__bar {
  display:flex; height:12px; border-radius:999px; overflow:hidden; margin-top:14px;
  background:var(--muted-soft);
}
.land__bar-seg { display:block; height:100%; }
.land__bar-seg--compost  { background:var(--ok); }
.land__bar-seg--recycle  { background:var(--brand-500); }
.land__bar-seg--landfill { background:var(--text-3); }
.land__key {
  list-style:none; display:flex; flex-wrap:wrap; gap:12px;
  margin:9px 0 0; padding:0; font-size:11.5px; color:var(--text-2);
}
.land__key li { display:inline-flex; align-items:center; gap:5px; }
.land__key-dot { width:9px; height:9px; border-radius:50%; display:inline-block; }
.land__key-dot--compost  { background:var(--ok); }
.land__key-dot--recycle  { background:var(--brand-500); }
.land__key-dot--landfill { background:var(--text-3); }

/* The button again at the foot: three screens of reading is too far to scroll
   back, and asking somebody to is asking them not to bother. */
.land__go--foot { margin-top:30px; }

@media (min-width:560px) {
  .land__figs { grid-template-columns:repeat(4, 1fr); }
}
@media (max-width:420px) {
  .land__ward { grid-template-columns:1fr; }
  .land__ward-when { justify-self:start; margin-top:3px; }
}

/* ==== the citizens' website ===============================================
   Its own shell rather than the form's. The form is a one-handed column and
   should stay one — somebody standing next to a bin does not want a layout.
   This is the public face of the service: linked from leaflets, opened on a
   desktop at a counter, projected in a ward meeting and scrolled on a handset,
   so it is a page with a header, a hero, a container and a footer, and it uses
   the width when there is width to use.

   Mobile first throughout: every grid below starts as one column and gains
   columns at a breakpoint, so the phone case is the plain case rather than the
   exception being undone. */

.site { min-height:100vh; min-height:100dvh; background:var(--surface-2); display:flex; flex-direction:column; }
.site__body { flex:1 1 auto; width:100%; max-width:1120px; margin:0 auto; padding:0 16px 8px; }

/* ---- header ---- */
.site__head {
  position:sticky; top:0; z-index:20;
  background:linear-gradient(135deg, #075045, #0f8a74); color:#fff;
  padding-top:env(safe-area-inset-top, 0px);
}
.site__head-in {
  display:flex; align-items:center; gap:14px;
  max-width:1120px; margin:0 auto; padding:11px 16px;
}
.site__brand { display:flex; align-items:center; gap:10px; min-width:0; color:inherit; text-decoration:none; }
.site__mark {
  width:36px; height:36px; flex:0 0 36px; border-radius:10px; display:grid; place-items:center;
  background:rgba(255,255,255,.16);
}
.site__brand-text { min-width:0; }
.site__org { display:block; font-weight:700; font-size:14.5px; line-height:1.3; }
.site__sub { display:block; font-size:11.5px; opacity:.85; }
/* Hidden on a phone: five jump links wrapping under a logo is a menu nobody
   reads, and the sections are a thumb-flick apart anyway. */
.site__nav { display:none; }
.site__head-acts { display:flex; align-items:center; gap:9px; margin-inline-start:auto; }
.site__head-cta {
  display:none; padding:7px 14px; border-radius:999px; font-size:13px; font-weight:700;
  background:#fff; color:var(--brand-700); text-decoration:none; white-space:nowrap;
}
.site__head-cta:hover { background:var(--brand-50); }

/* ---- hero ---- */
.site__hero { background:linear-gradient(180deg, #0f8a74 0%, #0b6f5e 100%); color:#fff; }
.site__hero-in {
  max-width:1120px; margin:0 auto; padding:26px 16px 30px;
  display:grid; gap:22px;
}
.site__h1 { font-size:26px; line-height:1.25; margin:0 0 8px; font-weight:800; }
.site__lead { font-size:15px; line-height:1.6; margin:0 0 18px; opacity:.92; max-width:56ch; }
.site__hero-acts { display:flex; gap:10px; flex-wrap:wrap; }
.site__cta {
  display:inline-flex; align-items:center; justify-content:center; gap:9px;
  padding:14px 22px; font-size:15.5px; font-weight:700; border-radius:10px;
}
.site__cta--ghost { background:rgba(255,255,255,.14); color:#fff; border-color:rgba(255,255,255,.45); }
.site__cta--ghost:hover { background:rgba(255,255,255,.22); color:#fff; }
.site__hero-note { margin:12px 0 0; font-size:12.5px; opacity:.8; }

/* The headline figures, in the hero where they answer "is this real" before
   anybody has scrolled. */
.site__hero-figs {
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(2, 1fr); gap:10px;
}
.site__hero-figs li {
  padding:13px 14px; border-radius:12px; text-align:center;
  background:rgba(255,255,255,.12); border:1px solid rgba(255,255,255,.2);
}
.site__hero-figs li.is-good { background:rgba(255,255,255,.2); border-color:rgba(255,255,255,.45); }
.site__hero-figs span {
  display:block; font-size:24px; font-weight:800; line-height:1.15;
  font-variant-numeric:tabular-nums;
}
.site__hero-figs small { display:block; margin-top:3px; font-size:11px; opacity:.85; line-height:1.35; }

/* ---- sections ---- */
.site__sec { padding:30px 0 4px; scroll-margin-top:74px; }
.site__sec + .site__sec { border-top:1px solid var(--line); }
.site__h2 { font-size:19px; font-weight:750; margin:0 0 5px; }
.site__h3 { font-size:14.5px; font-weight:700; margin:0 0 9px; }
.site__note { margin:0 0 15px; font-size:13.5px; line-height:1.6; color:var(--text-2); max-width:66ch; }
.site__aside { margin:14px 0 0; font-size:12.5px; color:var(--text-3); }
.site__two { display:grid; gap:22px; }

/* ---- wards ---- */
.site__wards { list-style:none; margin:0; padding:0; display:grid; gap:10px; }
.wcard {
  padding:13px 15px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
.wcard__name { font-size:14.5px; font-weight:700; }
.wcard__when { display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin-top:7px; }
.wcard__freq {
  font-size:11px; font-weight:700; padding:3px 9px; border-radius:999px;
  background:var(--brand-50); color:var(--brand-700);
}
.wcard__freq--none { background:var(--muted-soft); color:var(--text-3); font-weight:600; }
/* nowrap: the times read "6:00 am–10:00 am" rather than "06:00–10:00", which is
   long enough to break after the dash and leave an opening time alone on a line. */
.wcard__time { font-size:15px; font-weight:750; font-variant-numeric:tabular-nums; white-space:nowrap; }
/* A row that wraps rather than a column: on a phone the nine cards are the
   longest thing on the page, and two facts side by side is a card a third
   shorter with nothing lost. */
.wcard__facts {
  display:flex; flex-wrap:wrap; gap:3px 14px; margin-top:8px;
  font-size:11.5px; color:var(--text-3);
}
.wcard__facts span { display:inline-flex; align-items:center; gap:5px; }
.wcard__facts svg { flex:0 0 auto; }

/* ---- what goes where ---- */
.site__types { list-style:none; margin:0; padding:0; display:grid; gap:9px; grid-template-columns:1fr; }
.tcard {
  display:flex; align-items:center; gap:11px;
  padding:11px 13px; border-radius:var(--radius);
  border:1px solid color-mix(in srgb, var(--wt) 32%, var(--line));
  background:color-mix(in srgb, var(--wt) 7%, var(--surface));
}
.tcard__ic { display:flex; color:var(--wt); flex:0 0 auto; }
.tcard__name { font-size:13.5px; font-weight:650; flex:1 1 auto; min-width:0; }
.tcard__tag {
  font-size:10px; font-weight:700; letter-spacing:.02em; text-transform:uppercase;
  padding:2px 7px; border-radius:999px; background:var(--muted-soft); color:var(--text-3);
  flex:0 0 auto;
}
.tcard__tag.is-on { background:var(--ok-soft); color:var(--ok); }

/* ---- announcements ---- */
.site__news { list-style:none; margin:0; padding:0; display:grid; gap:14px; }
.ncard {
  border:1px solid var(--line); border-radius:var(--radius); overflow:hidden;
  background:var(--surface); display:flex; flex-direction:column;
}
/* Pinned: the accent on the left edge only, never a tinted frame all round. */
.ncard.is-pinned { border-inline-start:3px solid var(--brand-600); }
.ncard__img { width:100%; height:170px; object-fit:cover; display:block; }
.ncard__text { padding:13px 15px 15px; }
.ncard__meta {
  display:flex; align-items:center; gap:9px; flex-wrap:wrap;
  font-size:11px; color:var(--text-3);
}
.ncard__pin { display:inline-flex; align-items:center; gap:3px; color:var(--brand-700); font-weight:650; }
.ncard__t { margin:4px 0 0; font-size:15px; font-weight:700; line-height:1.4; }
.ncard__b {
  margin:6px 0 0; font-size:13.5px; line-height:1.6; color:var(--text-2);
  display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical; overflow:hidden;
}

/* The card is a link now, and the whole card is the target — see
   _news_card.ejs for why it is still only one link in the markup. The title
   keeps the page's text colour: a card that is entirely clickable does not need
   a blue line to say so, and nine blue headings in a grid read as a list of
   links rather than as the news. */
.ncard { position:relative; }
.ncard__t a { color:inherit; text-decoration:none; }
.ncard__t a::after { content:""; position:absolute; inset:0; border-radius:var(--radius); }
.ncard:hover .ncard__t a, .ncard__t a:focus-visible { text-decoration:underline; }
.ncard__t a:focus-visible { outline:none; }
.ncard:has(.ncard__t a:focus-visible) {
  outline:2px solid var(--brand-600); outline-offset:2px;
}
.ncard__go {
  display:inline-block; margin-top:9px; font-size:12px; font-weight:650;
  color:var(--brand-700);
}

/* ---- an announcement's own page ---- */
/* Left-aligned to the container like every other page of the site, not
   centred: a column that starts 180px in from the header's logo reads as a
   different site. Still a reading width. */
/* A row of its own: as inline-flex it shared a line with the eyebrow after it. */
.npage__back {
  display:flex; width:max-content; align-items:center; gap:6px; margin-bottom:16px;
  font-size:13px; font-weight:600; color:var(--text-2); text-decoration:none;
}
.npage__back:hover { color:var(--brand-700); }
/* The notice on one card the width of the page: it shares its left and right
   edges with the announcements and the band below it. The text inside keeps a
   reading width. */
.site__body > .npage__back { margin-top:20px; }
.npage {
  margin:0; padding:24px 28px 28px; border-radius:calc(var(--radius) + 4px);
  background:var(--surface); border:1px solid var(--line);
  box-shadow:0 6px 22px rgb(15 23 42 / .05), 0 1px 2px rgb(15 23 42 / .04);
}
.npage__t { max-width:900px; }
.npage__body { max-width:760px; }
.npage__tags { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.npage__eb { margin:0; }
.npage__pin {
  display:inline-flex; align-items:center; gap:5px; padding:3px 9px; border-radius:999px;
  background:var(--brand-600); color:#fff; font-size:11.5px; font-weight:700;
}
.npage__t {
  margin:10px 0 0; font-size:clamp(20px, 2.4vw, 26px); font-weight:800;
  line-height:1.35; letter-spacing:-0.005em; overflow-wrap:anywhere; color:var(--text-1);
}
.npage__bar {
  display:flex; align-items:center; justify-content:space-between; gap:10px 16px; flex-wrap:wrap;
  margin-top:12px; padding-bottom:14px; border-bottom:1px solid var(--line);
}
.npage__date { display:inline-flex; align-items:center; gap:6px; font-size:13px; color:var(--text-2); }
.npage__date svg { color:var(--brand-600); }
/* sharing: three small buttons on the date line */
.nshare { display:flex; align-items:center; gap:6px; }
.nshare__l { font-size:12px; font-weight:600; color:var(--text-3); margin-inline-end:2px; }
.nshare__b {
  display:inline-flex; align-items:center; justify-content:center; gap:6px; height:32px; min-width:32px; padding:0 8px;
  border-radius:8px; border:1px solid var(--line); background:var(--surface); color:var(--text-2);
  font:inherit; font-size:12.5px; font-weight:600; text-decoration:none; cursor:pointer;
  transition:border-color .15s, background .15s, color .15s;
}
.nshare__b--wa { color:#1faa53; } .nshare__b--fb { color:#1877f2; } .nshare__b--copy svg { color:var(--brand-600); }
.nshare__b:hover { border-color:var(--brand-500); background:color-mix(in srgb, var(--brand-500) 6%, var(--surface)); }
.nshare__b.is-done { border-color:var(--brand-600); color:var(--brand-700); background:color-mix(in srgb, var(--brand-500) 10%, var(--surface)); }
.npage__fig { margin:18px 0 0; }
.npage__img {
  width:100%; height:auto; max-height:440px; object-fit:cover; display:block;
  border-radius:var(--radius); background:var(--surface-2);
}
.npage__body { margin-top:18px; font-size:15px; line-height:1.8; color:var(--text-1); }
.npage__body p { margin:0 0 12px; overflow-wrap:anywhere; white-space:pre-line; }
.npage__body p:last-child { margin-bottom:0; }
.npage__body .npage__lead { font-size:16px; font-weight:500; }
@media (max-width:640px) {
  .npage { padding:16px 16px 20px; border-radius:var(--radius); }
  .npage__t { font-size:20px; }
  .npage__fig { margin:14px -16px 0; }
  .npage__img { border-radius:0; max-height:260px; }
  .npage__body { font-size:14.5px; line-height:1.75; }
  .npage__body .npage__lead { font-size:15px; }
  .nshare__l { display:none; }
}
.npage__more { border-top:0; margin-top:14px; }

/* ---- the split ---- */
.site__split {
  padding:16px; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface);
}
.site__key {
  list-style:none; display:flex; flex-wrap:wrap; gap:8px 18px;
  margin:12px 0 0; padding:0; font-size:12.5px; color:var(--text-2);
}
.site__key li { display:inline-flex; align-items:center; gap:6px; }
.site__key b { font-variant-numeric:tabular-nums; color:var(--text-1); }

/* ---- steps and the code ---- */
.site__steps { list-style:none; margin:0; padding:0; display:grid; gap:10px; }
.scard {
  display:flex; gap:12px; align-items:flex-start;
  padding:13px 14px; border-radius:var(--radius);
  background:var(--surface); border:1px solid var(--line);
}
.scard__n {
  flex:0 0 auto; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:var(--brand-600); color:#fff; font-size:12.5px; font-weight:700;
}
.scard__ic { display:flex; color:var(--brand-700); flex:0 0 auto; margin-top:4px; }
.scard__t { font-size:14px; font-weight:700; }
.scard__p { margin:3px 0 0; font-size:13px; line-height:1.6; color:var(--text-2); }
/* The code, boxed like the figures band above it rather than floating beside a
   numbered list. Centred against the square instead of pinned to its top, which
   left two short lines hanging above 90px of nothing. */
.site__qr {
  display:flex; gap:16px; align-items:center; flex-wrap:wrap;
  padding:16px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface);
}
/* Large: this is the square somebody photographs off a counter screen or a
   projected slide, and 144px on a monitor was a thumbnail. */
.site__qr-code {
  flex:0 0 auto; width:176px; height:176px; padding:10px;
  background:#fff; border:1px solid var(--line); border-radius:var(--radius);
}
@media (min-width:640px) { .site__qr-code { width:220px; height:220px; padding:12px; } }
.site__qr-code svg { width:100%; height:100%; display:block; }
/* It had no class at all, so it had no width of its own: a long address pushed
   the square instead of wrapping under it. */
.site__qr-text {
  flex:1 1 220px; min-width:0;
  display:flex; flex-direction:column; align-items:flex-start; gap:9px;
}
/* The note carries the band's spacing everywhere else on this page; inside this
   block the gap above does the same job. */
.site__qr-text .site__note { margin:0; }

/* ---- contacts ---- */
.site__contacts { list-style:none; margin:0; padding:0; display:grid; gap:10px; }
.ccard__main { min-width:0; }
.ccard__name { font-size:14.5px; font-weight:700; }
.ccard__title { font-size:12.5px; color:var(--text-2); margin-top:2px; }
.ccard__hours { font-size:11.5px; color:var(--text-3); margin-top:3px; }
.site__office {
  padding:15px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface); font-size:13.5px; align-self:start;
}

/* ---- the closer and the footer ---- */
.site__closer {
  margin:34px 0 8px; padding:30px 20px; border-radius:var(--radius);
  background:linear-gradient(120deg, var(--brand-800), var(--brand-600));
  color:#fff; text-align:center; position:relative; overflow:hidden;
}
/* A soft ring of light off to one side, so the band is a surface rather than
   a flat fill of the header's green. */
.site__closer::before {
  content:''; position:absolute; width:340px; height:340px; border-radius:50%;
  right:-90px; top:-190px; background:rgba(255,255,255,.08); pointer-events:none;
}
.site__closer-h { font-size:19px; font-weight:750; margin:0 0 16px; position:relative; }
.site__closer .site__cta { background:#fff; color:var(--brand-800); border-color:#fff; position:relative; }
.site__closer .site__cta:hover { background:var(--brand-50); color:var(--brand-800); }
.site__foot { margin-top:26px; background:#06322b; color:rgba(255,255,255,.72); }
.site__foot-in {
  max-width:1120px; margin:0 auto; padding:22px 16px calc(24px + env(safe-area-inset-bottom, 0px));
  display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap;
}
.site__foot-org { color:#fff; font-weight:700; font-size:14px; }
.site__foot-privacy { margin:7px 0 0; font-size:11.5px; line-height:1.65; max-width:70ch; }
.site__foot-staff {
  margin-inline-start:auto; font-size:11.5px; color:rgba(255,255,255,.6);
  text-decoration:underline; white-space:nowrap;
  /* Quiet by design, but a link is still a target: padding rather than a
     larger face, so it stays the small last thing and can still be pressed. */
  display:inline-block; padding:10px 0; margin-block:-10px;
}
.site__foot-staff:hover { color:#fff; }

/* ---- where there is room ------------------------------------------------ */
@media (min-width:560px) {
  .site__wards { grid-template-columns:repeat(2, 1fr); }
}
@media (min-width:640px) {
  .site__hero-figs { grid-template-columns:repeat(4, 1fr); }
  .site__types { grid-template-columns:repeat(2, 1fr); }
  .site__news { grid-template-columns:repeat(2, 1fr); }
  .site__h1 { font-size:clamp(31px, 3.2vw, 40px); letter-spacing:-0.01em; }
  .site__lead { font-size:16.5px; }
}
@media (min-width:900px) {
  .site__nav { display:flex; gap:18px; margin-inline-start:22px; }
  .site__nav a {
    color:rgba(255,255,255,.85); text-decoration:none; font-size:13px; font-weight:600;
    padding:6px 0; border-bottom:2px solid transparent;
  }
  .site__nav a:hover { color:#fff; border-bottom-color:rgba(255,255,255,.7); }
  .site__head-cta { display:inline-block; }
  /* The hero becomes two columns: the words on the left, the figures beside
     them rather than under, which is what stops a wide screen looking like a
     phone screen that has been stretched. */
  .site__hero-in { grid-template-columns:1.35fr 1fr; align-items:center; gap:36px; padding:40px 16px 44px; }
  .site__hero-figs { grid-template-columns:repeat(2, 1fr); gap:12px; }
  .site__wards { grid-template-columns:repeat(3, 1fr); }
  .site__types { grid-template-columns:repeat(3, 1fr); }
  .site__two { grid-template-columns:1fr 1fr; gap:30px; }
  .site__sec { padding:38px 0 6px; }
  .site__h2 { font-size:21px; }
}
@media (min-width:1100px) {
  .site__news { grid-template-columns:repeat(3, 1fr); }
}

/* ---- photographs --------------------------------------------------------
   Downloaded and served from this origin rather than hotlinked from a photo
   CDN. A municipality's own homepage should not depend on a server in another
   country to render, should not be slower than it has to be on a Teknaf mobile
   link, and should not tell a third party who visits it. They are stripped of
   metadata, progressive, and sized for the widest place they are drawn. */

.site__hero { position:relative; isolation:isolate; overflow:hidden; }
.site__hero-img {
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  z-index:-2;
}
/* The scrim. Dark enough at every point that white text over the busiest part
   of the photograph still clears the contrast threshold, and green rather than
   black so the page keeps one palette. */
.site__hero::after {
  content:''; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(180deg,
    rgba(4,48,40,.82) 0%, rgba(5,58,49,.74) 45%, rgba(7,72,60,.80) 100%);
}
/* Slightly stronger where the figures sit, which is the lighter half of most
   photographs. */
@media (min-width:900px) {
  .site__hero::after {
    /* Darkest on the left, where the headline sits; lighter on the right, so
       the photograph is actually visible behind the figures. A scrim heavy
       enough to hide the picture entirely is a coloured rectangle with a
       download attached. */
    background:linear-gradient(105deg,
      rgba(4,48,40,.90) 0%, rgba(5,58,49,.78) 52%, rgba(7,72,60,.62) 100%);
  }
}

/* A band across a section: enough to break up a wall of text, never enough to
   push the content somebody came for below the fold. */
.site__band {
  display:block; width:100%; height:150px; object-fit:cover;
  border-radius:var(--radius); margin:0 0 18px;
  background:var(--muted-soft);
}
@media (min-width:640px) { .site__band { height:190px; } }
@media (min-width:900px) { .site__band { height:220px; } }

.site__foot-credit { margin:6px 0 0; font-size:10.5px; color:rgba(255,255,255,.45); }

/* Nobody should be made to download a decorative photograph to read a page. */
@media (prefers-reduced-data:reduce) {
  .site__band { display:none; }
  .site__hero-img { display:none; }
}

/* The collection-times grid on /admin/landing. A table because it is read and
   fixed a row at a time: three wards wrong, six fine. */
.wgrid td { vertical-align:top; }
/* The ward's own column is the one that must be readable — it is what the row
   is about — so it is the one column with a floor. Everything else is a
   control of a known width; this was the column that gave way. */
.wgrid__ward { min-width:170px; }
.wgrid__name { font-weight:650; font-size:13px; }
/* Where the row's answer comes from, under the ward's name rather than in a
   column of its own: it is a fact about this row, read beside the name. */
.wgrid__src { margin-top:6px; display:flex; flex-wrap:wrap; gap:4px; }
/* From and to as one cell: two boxes and a dash, the way hours are written. */
.wgrid__hours { white-space:nowrap; }
.wgrid__hours input[type="time"] { width:104px; display:inline-block; }
.wgrid__hours-dash { display:inline-block; padding:0 4px; color:var(--text-3); }
.wgrid__sub { font-size:11px; color:var(--text-3); margin-top:2px; }
.wgrid__mid { vertical-align:middle; }
.wgrid input[type="time"] { min-width:104px; }
/* Wide enough for "From the rounds", which is what the select says on every
   row nobody has touched, and was cut to "From the r" at 104px. */
.wgrid select { min-width:148px; }
.wgrid__note { min-width:180px; }
.wgrid__note input { width:100%; }
.wgrid__note input + input { margin-top:5px; }
/* On the public page: what the two times could not say. */
.wcard__note { color:var(--text-2); }
/* What the page currently tells citizens for this ward, so an empty row is
   visibly a fallback rather than a blank. */
.wgrid__now { color:var(--brand-700); font-weight:650; }

/* ---- the grid a citizen navigates by ----
   Four cards under the hero: the four things anybody comes here for. On a
   phone it is the whole navigation, because the header's jump links are hidden
   at that width — five of them wrapping under a logo is a menu nobody reads. */
.site__grid {
  list-style:none; margin:-14px 0 0; padding:0; position:relative; z-index:1;
  display:grid; grid-template-columns:repeat(2, 1fr); gap:10px;
}
.gcard {
  display:flex; flex-direction:column; gap:3px; height:100%;
  padding:14px 14px 15px; border-radius:var(--radius);
  background:var(--surface); border:1px solid var(--line);
  box-shadow:0 2px 10px rgb(15 23 42 / .06); text-decoration:none; color:inherit;
}
.gcard:hover { border-color:var(--brand-500); box-shadow:0 4px 16px rgb(15 23 42 / .1); }
.gcard__ic { display:flex; color:var(--brand-600); }
.gcard__t { font-size:13.5px; font-weight:700; line-height:1.35; margin-top:5px; }
.gcard__p { font-size:11.5px; color:var(--text-3); line-height:1.45; }
/* The one that is an action rather than a jump. */
.gcard--go { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
.gcard--go:hover { background:var(--brand-700); border-color:var(--brand-700); }
.gcard--go .gcard__ic, .gcard--go .gcard__p { color:rgba(255,255,255,.85); }

@media (min-width:640px) { .site__grid { grid-template-columns:repeat(4, 1fr); } }

/* ==== the public page's editor: tabs ======================================
   Four separate jobs done on four separate days — the notice, the ward times,
   the announcements, the contacts — so one at a time rather than four screens
   of column. A grid rather than a row of links: four equal targets read in one
   pass, and on a phone they stack into two columns instead of wrapping into a
   ragged line. */
/* Flex, wrapping, and each tab stretches to fill its row. It was a grid whose
   column count was edited every time a tab was added — four, then five, then
   six across — and the seventh tab sat alone on a second row at the width
   this screen is mostly used at, which reads as a different kind of thing
   from the six above it. A wrapping flex row cannot leave a lonely stub: the
   last row's tabs widen to fill it, and there is no number here to update. */
.ltabs { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:14px; }
.ltab {
  flex:1 1 130px; min-width:0;
  display:flex; align-items:center; gap:9px;
  padding:10px 12px; border-radius:var(--radius);
  background:var(--surface); border:1px solid var(--line);
  text-decoration:none; color:var(--text-2); font-weight:650; font-size:13px; line-height:1.25;
}
/* A tab is not a link to somewhere else, and must not underline on hover — or
   on a phone, after a tap, where :hover sticks to the thing just pressed. */
.ltab:hover { border-color:var(--brand-500); color:var(--text-1); text-decoration:none; }
.ltab.is-on {
  background:var(--brand-600); border-color:var(--brand-600); color:#fff;
  box-shadow:0 2px 8px color-mix(in srgb, var(--brand-600) 30%, transparent);
}
.ltab__ic { display:flex; flex:0 0 auto; }
.ltab__t { flex:1 1 auto; min-width:0; }
.ltab__n {
  font-size:11px; font-weight:700; padding:1px 7px; border-radius:999px;
  background:var(--muted-soft); color:var(--text-3);
}
.ltab.is-on .ltab__n { background:rgba(255,255,255,.24); color:#fff; }

/* The master switch, above the tabs, because it decides whether anything under
   them is published at all. */
.lmaster { margin-bottom:12px; }
.lmaster__card {
  flex-direction:row; align-items:center; gap:14px; padding:13px 16px;
}
.lmaster__text { flex:1 1 auto; min-width:0; }
.lmaster__t { font-weight:700; font-size:14px; }
.lmaster__p { margin:2px 0 0; font-size:12px; color:var(--text-2); line-height:1.5; }


/* ---- the side navigation, on a phone -------------------------------------
   Five jump links wrapped under a logo is a menu nobody reads, so below 900px
   they move into a drawer behind one button. Above it the links are on the bar
   and the button is not drawn at all. */
.site__burger {
  /* First in the bar, so it is where the drawer comes from and under the
     thumb that reaches for it. */
  order:-1; margin-inline-end:2px;
  display:inline-flex; align-items:center; justify-content:center;
  width:40px; height:40px; padding:0; border-radius:10px; cursor:pointer;
  background:rgba(255,255,255,.14); border:1px solid rgba(255,255,255,.28); color:#fff;
}
.site__burger:hover { background:rgba(255,255,255,.24); }

.site__scrim {
  position:fixed; inset:0; z-index:40; background:rgb(4 24 20 / .5);
  opacity:0; transition:opacity .2s ease; backdrop-filter:blur(2px);
}
body.is-drawer .site__scrim { opacity:1; }

/* From the left, like the staff sidebar this municipality already uses, and
   from the same side as the button that opens it. Logical properties, so a
   right-to-left language gets it from the right without a second rule. */
.site__drawer {
  position:fixed; z-index:41; inset-block:0; inset-inline-start:0;
  width:min(84vw, 320px); display:flex; flex-direction:column;
  background:var(--surface); box-shadow:8px 0 30px rgb(4 24 20 / .28);
  transform:translateX(-100%); transition:transform .22s ease;
  padding-top:env(safe-area-inset-top, 0px);
}
[dir="rtl"] .site__drawer { transform:translateX(100%); box-shadow:-8px 0 30px rgb(4 24 20 / .28); }
body.is-drawer .site__drawer { transform:translateX(0); }
/* The page behind a drawer should not scroll under it. */
body.is-drawer { overflow:hidden; }

.site__drawer-head {
  display:flex; align-items:center; gap:10px; padding:14px 16px;
  border-bottom:1px solid var(--line);
}
.site__drawer-org { font-weight:700; font-size:14px; flex:1 1 auto; min-width:0; }
.site__drawer-x {
  width:34px; height:34px; border:0; background:transparent; cursor:pointer;
  font-size:24px; line-height:1; color:var(--text-3); border-radius:8px;
}
.site__drawer-x:hover { background:var(--surface-2); color:var(--text-1); }

.site__drawer-nav { flex:1 1 auto; overflow-y:auto; padding:14px 12px; }
.site__drawer-go {
  display:flex; align-items:center; justify-content:center; gap:9px;
  padding:13px 16px; margin-bottom:12px; border-radius:10px;
  background:var(--brand-600); color:#fff; font-weight:700; font-size:14.5px;
  text-decoration:none;
}
.site__drawer-go:hover { background:var(--brand-700); color:#fff; }
.site__drawer-link {
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:13px 14px; border-radius:9px; text-decoration:none;
  color:var(--text-1); font-weight:650; font-size:14px;
}
.site__drawer-link + .site__drawer-link { margin-top:2px; }
.site__drawer-link:hover { background:var(--brand-50); color:var(--brand-700); }
.site__drawer-chev { display:flex; color:var(--text-3); }
.site__drawer-foot {
  padding:14px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  border-top:1px solid var(--line);
}
.site__drawer-foot a { font-size:12px; color:var(--text-3); text-decoration:underline; }

@media (min-width:900px) {
  /* The links are on the bar up here, so the handle and its drawer are not
     drawn — and cannot be opened by a stale hash or a focused button. */
  .site__burger { display:none; }
  .site__drawer, .site__scrim { display:none !important; }
}

/* ---- rhythm -------------------------------------------------------------
   One flat grey from the hero to the footer reads as a long form. Alternating
   bands give the page a spine: a reader scrolling knows where one subject ends
   and the next begins without reading a word. The tint is full-bleed while the
   content inside it stays in the container. */
.site__sec--tint {
  background:var(--surface);
  border-top:1px solid var(--line); border-bottom:1px solid var(--line);
  /* Out to the edges of the viewport, and the padding puts the content back. */
  margin-inline:calc(50% - 50vw); padding-inline:calc(50vw - 50%);
}
.site__sec--tint + .site__sec { border-top:0; }
.site__sec + .site__sec--tint { border-top:1px solid var(--line); }

/* A small line above each heading saying what kind of thing this section is.
   Four words that orient somebody who has landed mid-page from a jump link. */
.site__eyebrow {
  display:inline-flex; align-items:center; gap:6px; margin:0 0 6px;
  font-size:11px; font-weight:750; letter-spacing:.07em; text-transform:uppercase;
  color:var(--brand-600);
}
.site__eyebrow svg { flex:0 0 auto; }

/* Cards lift slightly under the pointer. Small, because a municipal page is
   not a marketing site — enough to say a card is a target. */
.wcard, .ncard, .tcard { transition:transform .14s ease, box-shadow .14s ease, border-color .14s ease; }
.wcard:hover, .ncard:hover {
  transform:translateY(-1px); box-shadow:0 6px 18px rgb(15 23 42 / .08);
}
.gcard { transition:transform .14s ease, box-shadow .14s ease, border-color .14s ease; }
.gcard:hover { transform:translateY(-2px); }

/* ---- footer -------------------------------------------------------------
   Structured rather than a sentence: who this is, where to go, how to reach
   somebody — the three things a footer on a public body's site is for. */
.site__foot-in {
  display:grid; gap:20px; align-items:start;
  max-width:1120px; margin:0 auto; padding:26px 16px 22px;
}
.site__foot-brand { display:flex; align-items:center; gap:11px; }
.site__foot-mark {
  width:34px; height:34px; flex:0 0 34px; border-radius:9px; display:grid; place-items:center;
  background:rgba(255,255,255,.12); color:#fff;
}
.site__foot-org { color:#fff; font-weight:700; font-size:14.5px; }
.site__foot-strap { font-size:11.5px; opacity:.7; margin-top:1px; }
.site__foot-nav { display:flex; flex-direction:column; gap:7px; }
.site__foot-nav a { color:rgba(255,255,255,.75); text-decoration:none; font-size:12.5px; }
.site__foot-nav a:hover { color:#fff; text-decoration:underline; }
.site__foot-contact { display:flex; flex-direction:column; gap:9px; align-items:flex-start; }
.site__foot-tel {
  display:inline-flex; align-items:center; gap:7px;
  color:#fff; font-weight:700; font-size:14px; text-decoration:none;
}
.site__foot-tel:hover { text-decoration:underline; color:#fff; }
.site__foot-report {
  display:inline-block; padding:8px 15px; border-radius:999px;
  background:rgba(255,255,255,.14); border:1px solid rgba(255,255,255,.3);
  color:#fff; font-size:12.5px; font-weight:650; text-decoration:none;
}
.site__foot-report:hover { background:rgba(255,255,255,.24); color:#fff; }

.site__foot-legal { border-top:1px solid rgba(255,255,255,.12); }
.site__foot-legal-in {
  max-width:1120px; margin:0 auto;
  padding:14px 16px calc(18px + env(safe-area-inset-bottom, 0px));
  display:flex; gap:14px; align-items:flex-start; flex-wrap:wrap;
}
.site__foot-privacy { margin:0; font-size:11.5px; line-height:1.65; max-width:68ch; flex:1 1 320px; }
.site__foot-meta { display:flex; gap:14px; align-items:center; flex-wrap:wrap; margin-inline-start:auto; }
.site__foot-credit { font-size:10.5px; color:rgba(255,255,255,.45); margin:0; }

@media (min-width:760px) {
  .site__foot-in { grid-template-columns:1.4fr 1fr 1fr; }
  .site__sec--tint { padding-inline:calc(50vw - 50%); }
}
@media (prefers-reduced-motion:reduce) {
  .wcard, .ncard, .ccard, .gcard, .site__drawer, .site__scrim { transition:none; }
  .wcard:hover, .ncard:hover, .ccard:hover, .gcard:hover { transform:none; }
}

/* ---- skip to content ----
   The first focusable thing on the page and invisible until it is: a keyboard
   user should not have to tab through the drawer, the jump links and the hero
   to reach the page. */
.site__skip {
  position:absolute; inset-inline-start:12px; top:-60px; z-index:60;
  padding:10px 14px; border-radius:8px; font-weight:700; font-size:13.5px;
  background:#fff; color:var(--brand-700); box-shadow:var(--shadow);
  text-decoration:none; transition:top .12s ease;
}
.site__skip:focus-visible { top:12px; outline:2px solid var(--brand-500); outline-offset:2px; }
.site__body:focus { outline:none; }

/* ---- the preview bar ----
   Only on /admin/landing/preview. Amber, because it is the one thing on this
   page that is not the page, and it has to be told apart from the green header
   at a glance. Sticky above the header — the header moves down under it — so
   the way back to the editor is still there at the foot of a long page. */
.pbar {
  position:sticky; top:0; z-index:22;
  background:var(--warn-soft, #fff4d6); color:var(--text-1);
  border-bottom:1px solid color-mix(in srgb, var(--warn) 45%, var(--line));
  padding-top:env(safe-area-inset-top, 0px);
}
.pbar__in {
  display:flex; align-items:center; gap:10px; flex-wrap:nowrap;
  max-width:1120px; margin:0 auto; padding:0 16px; height:40px; min-width:0;
  font-size:12.5px;
}
.pbar__tag {
  display:inline-flex; align-items:center; gap:5px; flex:0 0 auto;
  padding:3px 9px; border-radius:999px; font-size:11px; font-weight:750;
  letter-spacing:.06em; text-transform:uppercase;
  background:var(--warn); color:#fff;
}
.pbar__state { display:inline-flex; align-items:center; gap:6px; flex:0 0 auto; font-weight:650; white-space:nowrap; }
.pbar__state i { width:8px; height:8px; border-radius:50%; background:var(--ok); flex:0 0 auto; }
.pbar__state.is-off i { background:var(--bad); }
.pbar__state.is-off { color:var(--bad); }
.pbar__note { flex:1 1 auto; min-width:0; color:var(--text-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.pbar__note b { color:var(--text-1); font-weight:650; }
.pbar__back {
  display:inline-flex; align-items:center; gap:5px; flex:0 0 auto;
  padding:6px 11px; border-radius:999px; font-weight:700; font-size:12.5px;
  background:var(--surface); border:1px solid var(--line); color:var(--text-1);
  text-decoration:none; white-space:nowrap;
}
.pbar__back:hover { border-color:var(--brand-500); color:var(--brand-700); }
/* The header sits under the bar, and a jump link lands under both. */
.site--preview .site__head { top:calc(40px + env(safe-area-inset-top, 0px)); }
.site--preview .site__sec { scroll-margin-top:114px; }
@media (max-width:640px) {
  /* Room for the state and the way back; the sentence is on the editor. */
  .pbar__note { display:none; }
  .pbar__tag { padding:3px 7px; }
}
@media print { .pbar { display:none; } }

/* ---- readable for everyone ----
   The public page is read by people who did not choose to use a computer:
   a resident on a cheap phone in daylight, a councillor across a projector.
   So the type is larger than the staff screens', the small-caps labels are
   plain-word tags with a coloured icon, headings are bigger, and every card
   has an outline strong enough to be seen as a card. */
.site__h2 { font-size:22px; line-height:1.25; margin-bottom:6px; }
.site__note { font-size:15px; line-height:1.55; }
.site__aside { font-size:13.5px; }
.site__eyebrow {
  display:inline-flex; align-items:center; gap:7px; margin:0 0 10px;
  padding:5px 12px 5px 8px; border-radius:999px;
  font-size:13px; font-weight:700; letter-spacing:0; text-transform:none;
  background:color-mix(in srgb, var(--brand-600) 12%, #fff); color:var(--brand-700);
}
.site__eyebrow svg { width:16px; height:16px; }
.site__sec[data-tone] .site__eyebrow { background:color-mix(in srgb, var(--tone) 14%, #fff); color:color-mix(in srgb, var(--tone) 78%, #000); }
.lang-bn .site__eyebrow { font-size:13.5px; }
/* the navigation cards under the hero */
.gcard { padding:16px 16px 17px; border-width:1.5px; }
.gcard__t { font-size:15.5px; margin-top:8px; }
.gcard__p { font-size:12.5px; }
.gcard[data-tone] .gcard__ic { width:40px; height:40px; border-radius:11px; }
.gcard[data-tone] .gcard__ic svg { width:22px; height:22px; }
.gcard--go .gcard__ic { width:40px; height:40px; border-radius:11px; align-items:center; justify-content:center; background:rgba(255,255,255,.18); }
/* the cards themselves: a visible outline, larger names and times */
.wcard, .ncard, .tcard, .scard, .site__office { border-color:var(--line-strong); }
.wcard { padding:15px 17px; }
.wcard__name { font-size:16px; }
.wcard__time { font-size:18px; }
.wcard__freq { font-size:12px; padding:4px 10px; }
.wcard__facts { font-size:12.5px; }
.tcard__name { font-size:15px; }
.tcard__tag { font-size:10.5px; }
.ncard__t { font-size:16.5px; }
.ncard__b { font-size:14px; }
.ncard__go { font-size:13px; }
.ccard__name { font-size:16px; }
.ccard__title { font-size:13.5px; }
.scard__t { font-size:15.5px; }
.scard__p { font-size:14px; }
.scard__n { width:30px; height:30px; font-size:14px; }
.site__closer-h { font-size:21px; }
.site__hero-note { font-size:13.5px; }
@media (min-width:900px) { .site__h2 { font-size:25px; } .site__sec { padding-block:42px 10px; } }

/* ---- a colour per section ----
   The navigation cards under the hero each carry a hue, and the section a card
   jumps to carries the same one: the eyebrow, the accent on its cards and a
   faint wash at the top of a tinted band. The reader who tapped the blue card
   lands on the blue section. Colour is never the only carrier — every section
   keeps its icon and its words. */
.site__sec[data-tone] .site__eyebrow { color:var(--tone); }
.site__sec--tint[data-tone] {
  background:linear-gradient(180deg, color-mix(in srgb, var(--tone) 6%, var(--surface)) 0%, var(--surface) 220px);
}
.site__sec[data-tone] .wcard {
  border-inline-start:3px solid color-mix(in srgb, var(--tone) 55%, var(--line));
}

/* The frequency chip says which kind of round it is, in colour as well as in
   words: every day green, alternate days blue, weekly violet. */
.wcard__freq[data-freq="daily"]     { background:var(--ok-soft);   color:#15803d; }
.wcard__freq[data-freq="alternate"] { background:var(--info-soft); color:#0369a1; }
.wcard__freq[data-freq="weekly"]    { background:#ede9fe; color:#6d28d9; }
.wcard__freq[data-freq="custom"]    { background:var(--warn-soft); color:#b45309; }
.wcard__time { font-size:16px; }

/* A notice with no photograph: a plate the height of one, in the card's own
   hue, with a large quiet glyph — so a row of cards lines up whether or not
   somebody had a picture that day. */
.ncard__plate {
  height:170px; display:grid; place-items:center;
  background:linear-gradient(135deg, color-mix(in srgb, var(--tone) 22%, #fff), color-mix(in srgb, var(--tone) 55%, #fff));
  color:color-mix(in srgb, var(--tone) 70%, #000);
}
.ncard__plate svg { opacity:.55; }
.ncard[data-tone] .ncard__go { color:color-mix(in srgb, var(--tone) 75%, #000); }
.ncard[data-tone] .ncard__pin { color:color-mix(in srgb, var(--tone) 75%, #000); }
/* A news card's accent is a thin left border in its hue; the other three
   sides stay the plain line, on hover as well. */
.ncard[data-tone] { border-inline-start:3px solid color-mix(in srgb, var(--tone) 60%, var(--line)); }
.ncard[data-tone]:hover { border-inline-start-color:var(--tone); }
@media (max-width:640px) { .ncard__img, .ncard__plate { height:150px; } }

/* Initials in a colour each, so four cards are four people rather than four
   copies of the same green circle. */
.ccard[data-tone] .land__call, .ccard[data-tone] .land__mail { color:var(--text-1); }
.ccard[data-tone] .land__call:hover, .ccard[data-tone] .land__mail:hover { color:#fff; }
.site__office { border-inline-start:3px solid color-mix(in srgb, var(--brand-600) 55%, var(--line)); }
.site__office-ic {
  display:inline-grid; place-items:center; width:26px; height:26px; margin-inline-end:8px;
  border-radius:8px; background:var(--brand-50); color:var(--brand-700); vertical-align:middle;
}
.site__office .site__h3 { display:flex; align-items:center; }

/* ---- the rest of the announcements ---------------------------------------
   Under the three cards on the landing page: one button to the archive. On
   the archive itself: a newer/older pager with the page numbers between. */
.site__more { margin-top:16px; display:flex; justify-content:center; }
.site__more-btn {
  display:inline-flex; align-items:center; gap:8px; padding:10px 18px;
  border-radius:999px; font-weight:700; font-size:13.5px;
  background:var(--surface); border:1px solid var(--line); color:var(--text-1);
}
.site__more-btn:hover { border-color:var(--brand-500); color:var(--brand-700); }
.site__sec[data-tone] .site__more-btn:hover { border-color:var(--tone); color:color-mix(in srgb, var(--tone) 75%, #000); }
.nlist { padding-top:22px; }
.nlist__t { font-size:clamp(22px, 3.2vw, 28px); }
.nlist .npage__back { margin-bottom:14px; }
.npager {
  display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;
  margin-top:22px; padding-top:16px; border-top:1px solid var(--line);
}
.npager__arm {
  display:inline-flex; align-items:center; gap:6px; padding:9px 14px;
  border-radius:999px; font-weight:700; font-size:13.5px; text-decoration:none;
  background:var(--surface); border:1px solid var(--line); color:var(--text-1);
}
.npager__arm:hover { border-color:var(--brand-500); color:var(--brand-700); }
.npager__arm.is-off { opacity:.4; pointer-events:none; }
.npager__pages { display:flex; align-items:center; gap:4px; }
.npager__pages a, .npager__pages b {
  min-width:34px; height:34px; padding:0 8px; display:inline-grid; place-items:center;
  border-radius:8px; font-size:13.5px; font-weight:650; text-decoration:none; color:var(--text-2);
  font-variant-numeric:tabular-nums;
}
.npager__pages a:hover { background:var(--brand-50); color:var(--brand-700); }
.npager__pages b { background:var(--brand-600); color:#fff; }
.npager__pages i { color:var(--text-3); padding:0 4px; font-style:normal; }
.npager__where { margin:10px 0 0; text-align:center; font-size:12px; color:var(--text-3); }
@media (max-width:480px) {
  /* The arms take the whole width and the numbers sit between them. */
  .npager { justify-content:center; }
  .npager__pages { order:-1; flex:1 1 100%; justify-content:center; }
}

/* ---- no underline on hover across the public site ----
   The application's base rule underlines every link on hover, which is right
   for a word in a sentence and wrong for a card, a button, a logo or a jump
   link: hovering the "report a problem" card underlined three lines of it.
   Off everywhere on the site, then back on for the links that *are* words in
   a sentence — an aside, a paragraph, a footer line. */
.site a:hover { text-decoration:none; }
.site p a:hover, .site .site__aside a:hover, .site .npage__body a:hover,
.site .site__foot-nav a:hover, .site .site__foot-staff:hover, .site .site__foot-tel:hover,
.site .site__hero-track:hover { text-decoration:underline; }

/* ---- track your report ---------------------------------------------- */
.site__hero-track {
  display:inline-block; margin-inline-start:10px; color:#fff; font-weight:700; opacity:.95;
}
.track__form { display:flex; gap:10px; flex-wrap:wrap; max-width:640px; }
.track__q {
  flex:1 1 260px; min-width:0; padding:13px 15px; font-size:16px; border-radius:10px;
  border:1px solid var(--line-strong); background:var(--surface);
  font-variant-numeric:tabular-nums;
}
.track__q:focus { outline:2px solid var(--brand-500); outline-offset:1px; border-color:var(--brand-500); }
.track__go { flex:0 0 auto; padding:13px 20px; font-size:15px; font-weight:700; border-radius:10px; display:inline-flex; align-items:center; gap:8px; }
/* The page: a heading with a badge, then the box in a raised card with the
   search icon inside the field and a line under it saying what the page will
   and will not show. Focus is loud on purpose — a thick ring in the brand
   hue — because the page puts the cursor here for anyone who followed a
   "track" link, and they should see where it went. */
.track { padding-top:18px; position:relative; }
.track::before {
  content:''; position:absolute; z-index:-1; pointer-events:none;
  width:420px; height:420px; border-radius:50%; left:-220px; top:-120px;
  background:radial-gradient(circle, color-mix(in srgb, var(--tone) 10%, transparent), transparent 70%);
}
.track .npage__back { margin-bottom:14px; }
.track__head { display:flex; gap:18px; align-items:center; margin-bottom:18px; }
.track__badge {
  flex:0 0 auto; width:72px; height:72px; border-radius:50%; display:grid; place-items:center;
  background:color-mix(in srgb, var(--tone) 12%, var(--surface)); color:var(--tone);
  box-shadow:0 0 0 8px color-mix(in srgb, var(--tone) 5%, transparent);
}
.track__head-text { min-width:0; }
.track__t { font-size:clamp(22px, 3.4vw, 30px); margin:0; letter-spacing:-.01em; }
.track__lead { margin:6px 0 0; max-width:620px; font-size:15px; line-height:1.6; color:var(--text-2); }

.tpanel {
  max-width:820px; padding:14px; border-radius:calc(var(--radius) + 4px);
  background:var(--surface); border:1px solid var(--line);
  box-shadow:0 10px 30px rgb(15 23 42 / .07), 0 1px 2px rgb(15 23 42 / .04);
}
.track__form--panel { max-width:none; align-items:stretch; gap:10px; flex-wrap:nowrap; }
.track__field { position:relative; flex:1 1 auto; min-width:0; }
.track__field-ic {
  position:absolute; inset-inline-start:16px; top:50%; transform:translateY(-50%);
  display:flex; color:var(--text-3); pointer-events:none; transition:color .15s;
}
.track__field:focus-within .track__field-ic { color:var(--tone); }
/* One height for the box and the button, set outright: the application's own
   input rule sizes text boxes for staff forms and left this one shorter than
   the button beside it. */
.track__form--panel input.track__q, .track__form--panel .track__go { height:56px; box-sizing:border-box; margin:0; }
.track__form--panel input.track__q {
  width:100%; padding:0 16px; padding-inline-start:48px; font-size:17px; line-height:1; letter-spacing:.01em;
  border:1.5px solid var(--line-strong); border-radius:12px; background:var(--surface); color:var(--text-1);
  transition:border-color .15s, box-shadow .15s;
}
.track__form--panel input.track__q::placeholder { color:var(--text-3); opacity:1; }
.track__form--panel input.track__q:hover { border-color:color-mix(in srgb, var(--tone) 45%, var(--line-strong)); }
.track__form--panel input.track__q:focus {
  outline:none; border-color:var(--tone);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--tone) 22%, transparent);
}
.track__form--panel .track__go {
  flex:0 0 auto; padding:0 26px; font-size:16px; border-radius:12px; justify-content:center;
  background:var(--tone); border-color:var(--tone); color:#fff;
  box-shadow:0 6px 16px color-mix(in srgb, var(--tone) 30%, transparent);
}
.track__form--panel .track__go:hover { filter:brightness(.92); }
.track__form--panel .track__go:focus-visible { outline:3px solid color-mix(in srgb, var(--tone) 45%, transparent); outline-offset:2px; }
.tpanel__hint {
  display:flex; gap:8px; align-items:flex-start; margin:10px 0 0; padding:9px 12px; border-radius:10px;
  background:color-mix(in srgb, var(--tone) 7%, var(--surface)); border:1px solid color-mix(in srgb, var(--tone) 16%, var(--line));
  font-size:12.5px; line-height:1.5; color:var(--text-2);
}
.tpanel__hint > span:first-child { display:flex; flex:0 0 auto; color:var(--tone); margin-top:1px; }

/* Showing an answer: no badge or explanation, a smaller heading, a shallower
   box, so the report itself is on the screen. */
.track--tight { padding-top:14px; }
.track--tight .npage__back { margin-bottom:8px; }
.track--tight .track__head { margin-bottom:12px; }
.track--tight .track__t { font-size:21px; }
.track--tight .tpanel { padding:10px; }
.track--tight .track__form--panel input.track__q, .track--tight .track__form--panel .track__go { height:48px; }
.track--tight .track__form--panel input.track__q { font-size:16px; }
.track--tight .track__form--panel .track__go { font-size:15px; padding:0 20px; }
.track--tight .tpanel__hint { margin-top:8px; padding:6px 10px; }
.track--tight .tcase { margin-top:16px; padding:18px 20px 20px; }
.track--tight .tsteps { margin-top:16px; }
.track--tight .tcase__facts { margin-top:14px; padding-top:12px; gap:10px 18px; }
.track--tight .site__aside { margin-top:10px; }
@media (max-width:640px) {
  .track__head { gap:14px; }
  .track__badge { width:54px; height:54px; box-shadow:0 0 0 5px color-mix(in srgb, var(--tone) 5%, transparent); }
  .track__badge svg { width:24px; height:24px; }
  .track__lead { font-size:14px; }
  .tpanel { padding:12px; }
  .track__form--panel { flex-wrap:wrap; }
  .track__form--panel .track__go { width:100%; }
}
/* One reading column, so the page has one left edge and one right edge. */
.track .track__msg, .track .tpanel, .track .tresults, .track .tcase { max-width:820px; }
.track__msg {
  display:flex; gap:10px; align-items:flex-start;
  margin:16px 0 0; padding:12px 14px; border-radius:12px;
  background:var(--info-soft); border:1px solid #7dd3fc; color:#075985;
}
.track__msg--warn { background:var(--warn-soft); border-color:#fcd34d; color:#92400e; }
.track__msg span { display:flex; flex:0 0 auto; margin-top:1px; }
.track__msg p { margin:0; font-size:13.5px; line-height:1.55; }

/* one report */
.tcase {
  margin-top:22px; padding:20px 22px 22px;
  border:1px solid var(--line); border-radius:calc(var(--radius) + 4px); background:var(--surface);
  border-inline-start:4px solid var(--tone); box-shadow:0 4px 18px rgb(15 23 42 / .05);
}
.tcase__head { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
.tcase__ref { margin:0; font-size:24px; font-weight:800; letter-spacing:-.01em; font-variant-numeric:tabular-nums; }
.tcase__status {
  padding:6px 14px; border-radius:999px; font-weight:700; font-size:13.5px;
  background:color-mix(in srgb, var(--tone) 14%, #fff); color:color-mix(in srgb, var(--tone) 80%, #000);
}
.tsteps {
  list-style:none; margin:20px 0 0; padding:0; display:grid; grid-template-columns:repeat(3, 1fr);
  position:relative; counter-reset:none;
}
.tsteps__s { position:relative; text-align:center; padding-top:30px; }
.tsteps__dot {
  position:absolute; top:0; left:50%; transform:translateX(-50%);
  width:22px; height:22px; border-radius:50%; display:grid; place-items:center;
  background:var(--surface); border:2px solid var(--line-strong); color:#fff; z-index:1;
}
/* the rail between the dots */
.tsteps__s::before, .tsteps__s::after {
  content:''; position:absolute; top:10px; height:2px; background:var(--line);
}
.tsteps__s::before { left:0; right:50%; }
.tsteps__s::after { left:50%; right:0; }
.tsteps__s:first-child::before, .tsteps__s:last-child::after { display:none; }
.tsteps__s.is-done .tsteps__dot { background:var(--tone); border-color:var(--tone); }
.tsteps__s.is-done::before, .tsteps__s.is-done::after { background:var(--tone); }
.tsteps__s.is-now::after { background:var(--line); }
.tsteps__s.is-now .tsteps__dot { box-shadow:0 0 0 5px color-mix(in srgb, var(--tone) 22%, transparent); }
.tsteps__s.is-rejected .tsteps__dot { background:var(--bad); border-color:var(--bad); }
.tsteps__l { display:block; font-size:13px; font-weight:700; color:var(--text-3); }
.tsteps__s.is-done .tsteps__l { color:var(--text-1); }
.tsteps__d { display:block; font-size:11.5px; color:var(--text-3); margin-top:2px; }
.tcase__note { margin:16px 0 0; font-size:13.5px; line-height:1.55; color:var(--text-2); }
.tcase__note:empty { display:none; }
.tcase__facts {
  display:grid; grid-template-columns:repeat(auto-fit, minmax(128px, 1fr)); gap:12px 18px;
  margin:18px 0 0; padding-top:16px; border-top:1px solid var(--line);
}
.tcase__facts dt { font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--text-3); }
.tcase__facts dd { margin:3px 0 0; font-size:14px; font-weight:600; }
.tcase__outcome {
  margin-top:16px; padding:13px 15px; border-radius:var(--radius);
  background:color-mix(in srgb, var(--tone) 8%, var(--surface)); border:1px solid color-mix(in srgb, var(--tone) 30%, var(--line));
}
.tcase__outcome-l { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:color-mix(in srgb, var(--tone) 80%, #000); }
.tcase__outcome p { margin:6px 0 0; font-size:14px; line-height:1.6; white-space:pre-line; }
.tcase__h { margin-top:20px; }
.tlog { list-style:none; margin:0; padding:0; }
.tlog__i {
  display:flex; gap:14px; align-items:baseline; padding:7px 0; border-bottom:1px solid var(--line); font-size:13.5px;
}
.tlog__i:last-child { border-bottom:0; }
.tlog__i time { flex:0 0 auto; font-size:12px; color:var(--text-3); font-variant-numeric:tabular-nums; min-width:150px; }
.tlog__i span { font-weight:650; }

/* every report made with one number: one card holding a row per report —
   an icon for what it is about, the reference and date, where, the status. */
.tresults {
  margin-top:22px; padding:16px; border-radius:calc(var(--radius) + 4px);
  background:var(--surface); border:1px solid var(--line); box-shadow:0 4px 18px rgb(15 23 42 / .05);
}
.tresults__h { display:flex; align-items:center; gap:12px; margin:0 0 14px; font-size:17px; font-weight:750; line-height:1.4; }
.tresults__ic {
  flex:0 0 auto; width:38px; height:38px; border-radius:50%; display:grid; place-items:center;
  background:color-mix(in srgb, var(--tone) 12%, var(--surface)); color:var(--tone);
}
.tresults__n {
  display:inline-grid; place-items:center; min-width:30px; height:28px; padding:0 9px; margin-inline:6px 3px;
  border-radius:999px; background:var(--tone); color:#fff; font-size:15px; font-variant-numeric:tabular-nums;
  vertical-align:middle;
}
.tlist { list-style:none; margin:0; padding:0; display:grid; gap:10px; }
.tlist__i {
  display:grid; grid-template-columns:48px minmax(120px, 170px) 1fr auto 18px; gap:16px; align-items:center;
  padding:14px 16px; border:1px solid var(--line); border-radius:12px;
  background:var(--surface); color:inherit; border-inline-start:4px solid var(--tone);
  transition:border-color .15s, box-shadow .15s, transform .15s;
}
.tlist__i:hover { border-color:color-mix(in srgb, var(--tone) 45%, var(--line)); border-inline-start-color:var(--tone); box-shadow:0 6px 18px rgb(15 23 42 / .08); transform:translateY(-1px); }
.tlist__i:focus-visible { outline:3px solid color-mix(in srgb, var(--tone) 45%, transparent); outline-offset:2px; }
.tlist__ic {
  width:48px; height:48px; border-radius:12px; display:grid; place-items:center;
  background:color-mix(in srgb, var(--tone) 12%, var(--surface)); color:var(--tone);
}
.tlist__id { display:flex; flex-direction:column; gap:3px; min-width:0; padding-inline-end:16px; border-inline-end:1px solid var(--line); }
.tlist__ref { font-weight:800; font-size:16px; font-variant-numeric:tabular-nums; }
.tlist__when { font-size:12.5px; color:var(--text-3); }
.tlist__about { display:flex; flex-direction:column; gap:3px; min-width:0; }
.tlist__cat { font-size:15px; font-weight:700; }
.tlist__where { font-size:13px; color:var(--text-2); }
.tlist__st {
  display:inline-flex; align-items:center; gap:6px; padding:6px 13px; border-radius:999px;
  font-size:13px; font-weight:700; white-space:nowrap;
  background:color-mix(in srgb, var(--tone) 13%, var(--surface)); color:color-mix(in srgb, var(--tone) 80%, var(--text-1));
}
.tlist__go { display:flex; color:var(--text-3); transition:transform .15s, color .15s; }
.tlist__i:hover .tlist__go { color:var(--tone); transform:translateX(2px); }
@media (max-width:720px) {
  .tresults { padding:12px; }
  .tlist__i {
    grid-template-columns:44px 1fr 18px; grid-template-areas:"ic id go" "ic about go" "ic st go";
    gap:6px 12px; align-items:start; padding:12px;
  }
  .tlist__ic { grid-area:ic; width:44px; height:44px; }
  .tlist__id { grid-area:id; flex-direction:row; align-items:baseline; flex-wrap:wrap; gap:4px 10px; padding:0; border:0; }
  .tlist__about { grid-area:about; }
  .tlist__st { grid-area:st; justify-self:start; padding:4px 11px; font-size:12.5px; }
  .tlist__go { grid-area:go; align-self:center; }
}
@media (max-width:480px) {
  .tresults__h { font-size:15.5px; gap:10px; }
  .tsteps__l { font-size:12px; }
  .tlog__i { flex-direction:column; gap:2px; }
}

/* The closer on this page, laid out in a row: an icon, the question with a
   line under it, the button. */
.site__closer--row { display:flex; align-items:center; gap:20px; text-align:start; padding:26px 30px; }
.site__closer-ic {
  flex:0 0 auto; width:64px; height:64px; border-radius:50%; display:grid; place-items:center;
  background:rgba(255,255,255,.14); color:#fff; position:relative;
}
.site__closer-text { flex:1 1 auto; min-width:0; position:relative; }
.site__closer--row .site__closer-h { margin:0; }
.site__closer-p { margin:4px 0 0; font-size:14px; color:rgba(255,255,255,.82); }
.site__closer--row .site__cta { flex:0 0 auto; display:inline-flex; align-items:center; gap:8px; }
@media (max-width:640px) {
  .site__closer--row { flex-direction:column; text-align:center; padding:24px 18px; gap:14px; }
  .site__closer-ic { width:54px; height:54px; }
}
/* the thank-you page: something to keep */
.report__keep {
  display:flex; gap:16px; align-items:center; margin-top:18px; padding:14px;
  border:1px solid var(--line-strong); border-radius:var(--radius); background:var(--surface); text-align:start;
}
.report__keep-qr { flex:0 0 auto; width:96px; height:96px; padding:6px; background:#fff; border:1px solid var(--line); border-radius:8px; }
.report__keep-qr svg { width:100%; height:100%; display:block; }
.report__keep-text { flex:1 1 auto; min-width:0; }
.report__keep-t { font-size:14.5px; font-weight:750; }
.report__keep-p { margin:3px 0 10px; font-size:12.5px; color:var(--text-2); line-height:1.5; }
.report__keep-acts { display:flex; gap:8px; flex-wrap:wrap; }
.report__keep-acts .btn { display:inline-flex; align-items:center; gap:7px; }
@media (max-width:420px) { .report__keep { flex-direction:column; align-items:stretch; } .report__keep-qr { align-self:center; } .report__keep-acts .btn { flex:1 1 100%; justify-content:center; } }

/* ---- a complaint that is not about a place ----
   One run of pictures rather than two columns, and the note on the citizen's
   form that says who will read what they are about to write. */
.ba--single { grid-template-columns:1fr; }
.rstep[hidden] { display:none; }
.report__kindnote {
  margin:12px 0 0; padding:11px 13px; border-radius:var(--radius); font-size:13px; line-height:1.55;
  background:var(--info-soft); border:1px solid #7dd3fc; color:#075985;
}
.report__kindnote[data-kind="conduct"] { background:var(--warn-soft); border-color:#fcd34d; color:#92400e; }

/* ---- who to contact: the search and the cards ----
   A toolbar with a search box and the ward list, then cards with a ring
   round the initials in the card's own hue, the role as a chip, the hours
   with a clock, and call/mail as buttons a thumb can hit. */
.ctool { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:0 0 16px; }
.ctool__search {
  flex:1 1 260px; min-width:0; display:flex; align-items:center; gap:8px;
  padding:0 12px; height:46px; border-radius:12px;
  background:var(--surface); border:1.5px solid var(--line-strong);
}
.ctool__search:focus-within { border-color:var(--tone, var(--brand-600)); box-shadow:0 0 0 3px color-mix(in srgb, var(--tone, var(--brand-600)) 20%, transparent); }
.ctool__ic { display:flex; color:var(--text-3); flex:0 0 auto; }
.ctool__search input { flex:1 1 auto; min-width:0; border:0; background:transparent; font-size:15px; outline:none; padding:0; height:100%; }
.ctool__ward { flex:0 1 260px; min-width:200px; }
.ctool__ward select { width:100%; height:46px; border-radius:12px; border:1.5px solid var(--line-strong); font-size:14.5px; padding:0 12px; background:var(--surface); }
.ctool__clear { height:46px; border-radius:12px; }
/* The contact card. A coloured edge on the left in the card's own hue, the
   picture or initial as a rounded tile, name and role beside it, the hours
   under, and Call / Write as two equal buttons along the bottom — the two
   things a citizen came to the card to do, made large. */
/* The accent is an inset strip rather than a bar painted over the card, so it
   follows the rounded corners; the border is the same light line on all four
   sides, and a soft shadow lifts the card instead of a heavier edge. */
/* A plain rectangle: square corners, a real thin border on the left in the
   card's own colour — a border, not an inset shadow, the same way the news
   and ward cards carry theirs — and a light drop shadow to lift it off the
   page. The other three sides carry no border. */
.ccard {
  position:relative; display:flex; flex-direction:column; gap:12px; padding:16px 16px 14px 19px;
  border:0; border-inline-start:3px solid var(--tone, var(--brand-600));
  border-radius:0; background:var(--surface);
  box-shadow:0 2px 10px rgb(15 23 42 / .07);
  transition:transform .14s ease, box-shadow .14s ease;
}
.ccard::before { display:none; }
.ccard:hover { transform:translateY(-2px); box-shadow:0 12px 28px rgb(15 23 42 / .14); }
.ccard__top { display:flex; gap:14px; align-items:center; min-width:0; }
.ccard__pic {
  width:64px; height:64px; flex:0 0 64px; border-radius:14px; object-fit:cover;
  background:color-mix(in srgb, var(--tone, var(--brand-600)) 14%, #fff);
  box-shadow:inset 0 0 0 2px color-mix(in srgb, var(--tone, var(--brand-600)) 30%, #fff);
}
.ccard__pic--none {
  display:grid; place-items:center; font-size:26px; font-weight:800;
  color:color-mix(in srgb, var(--tone, var(--brand-600)) 80%, #000);
}
.ccard__main { min-width:0; display:flex; flex-direction:column; align-items:flex-start; gap:4px; }
.ccard__name { font-size:17px; font-weight:750; line-height:1.2; overflow-wrap:anywhere; }
.ccard__role {
  display:inline-block; padding:3px 9px; border-radius:999px; font-size:12px; font-weight:700; line-height:1.3;
  background:color-mix(in srgb, var(--tone, var(--brand-600)) 12%, #fff); color:color-mix(in srgb, var(--tone, var(--brand-600)) 78%, #000);
}
.ccard__hours { display:flex; align-items:center; gap:6px; font-size:13px; color:var(--text-2); }
.ccard__hours svg { color:var(--text-3); flex:0 0 auto; }
.ccard__acts { display:grid; grid-template-columns:1fr; gap:8px; margin-top:auto; }
.ccard__act {
  display:grid; grid-template-columns:auto 1fr; grid-template-areas:"ic l" "ic v"; column-gap:9px; row-gap:1px; align-items:center;
  padding:9px 12px; border-radius:11px; text-decoration:none; color:var(--text-1); min-width:0;
  background:var(--surface-2); border:1.5px solid var(--line);
}
.ccard__act svg { grid-area:ic; color:var(--tone, var(--brand-600)); }
.ccard__act-l { grid-area:l; font-size:11px; font-weight:750; letter-spacing:.05em; text-transform:uppercase; color:var(--text-3); }
.lang-bn .ccard__act-l { letter-spacing:0; font-size:12px; }
.ccard__act-v { grid-area:v; font-size:13.5px; font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-variant-numeric:tabular-nums; }
.ccard__act--call { background:color-mix(in srgb, var(--tone, var(--brand-600)) 10%, #fff); border-color:color-mix(in srgb, var(--tone, var(--brand-600)) 35%, var(--line)); }
.ccard__act:hover { background:var(--tone, var(--brand-600)); border-color:var(--tone, var(--brand-600)); color:#fff; text-decoration:none; }
.ccard__act:hover svg, .ccard__act:hover .ccard__act-l { color:#fff; }
.ccard[hidden] { display:none; }

/* ---- the announcements filter ----
   On the archive: a search box, the years, a button. On the landing page's
   band: the same box beside the "see all" button, opening the archive. */
.nfilter { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:0 0 18px; }
.nfilter__q {
  flex:1 1 260px; min-width:0; display:flex; align-items:center; gap:8px;
  padding:0 12px; height:46px; border-radius:12px;
  background:var(--surface); border:1.5px solid var(--line-strong);
}
.nfilter__q:focus-within { border-color:var(--tone, var(--brand-600)); box-shadow:0 0 0 3px color-mix(in srgb, var(--tone, var(--brand-600)) 20%, transparent); }
.nfilter__q input { flex:1 1 auto; min-width:0; border:0; background:transparent; font-size:15px; outline:none; padding:0; height:100%; }
.nfilter__year { flex:0 1 160px; min-width:130px; }
.nfilter__year select { width:100%; height:46px; border-radius:12px; border:1.5px solid var(--line-strong); font-size:14.5px; padding:0 12px; background:var(--surface); }
.nfilter__go, .nfilter__clear { height:46px; border-radius:12px; padding:0 18px; display:inline-flex; align-items:center; }
.nfilter__count { margin:-6px 0 14px; font-size:13.5px; color:var(--text-2); font-weight:600; }
.site__more--search { gap:10px; flex-wrap:wrap; }
.nfilter__q--band { flex:0 1 360px; height:44px; }
.site__more--search .site__more-btn { height:44px; cursor:pointer; }

/* ---- who to contact, at length ----
   The office as one bar across the top; the people in a grid, grouped by
   ward, with a filter once there are more than a handful. Twenty contacts in
   one column was a page nobody found their own ward on. */
/* The municipal office: a card with a green head and four tiles — where,
   when, the number and the address — the last two being buttons, because a
   number on a page held in a hand is something to press, not to read. */
.office {
  margin:0 0 20px; border-radius:16px; overflow:hidden;
  background:var(--surface); border:1px solid var(--line-strong);
  box-shadow:0 8px 24px rgb(15 23 42 / .06);
}
.office__head {
  display:flex; align-items:center; gap:14px; padding:16px 20px;
  background:linear-gradient(120deg, var(--brand-800), var(--brand-600)); color:#fff;
}
.office__badge {
  width:44px; height:44px; flex:0 0 44px; border-radius:12px; display:grid; place-items:center;
  background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.28);
}
.office__t { margin:0; font-size:17px; font-weight:750; color:#fff; }
.office__sub { margin:2px 0 0; font-size:12.5px; opacity:.85; }
.office__grid { display:grid; grid-template-columns:1fr; gap:1px; background:var(--line); }
.otile {
  display:flex; gap:12px; align-items:flex-start; padding:15px 18px; min-width:0;
  background:var(--surface); color:inherit; text-decoration:none;
}
.otile__ic {
  width:36px; height:36px; flex:0 0 36px; border-radius:10px; display:grid; place-items:center;
  background:var(--brand-50); color:var(--brand-700);
}
.otile__text { min-width:0; display:flex; flex-direction:column; gap:2px; }
.otile__l { font-size:11px; font-weight:750; letter-spacing:.06em; text-transform:uppercase; color:var(--text-3); }
.lang-bn .otile__l { letter-spacing:0; font-size:12px; }
.otile__v { font-size:14.5px; line-height:1.45; color:var(--text-1); overflow-wrap:anywhere; }
.otile__v--big { font-size:19px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap; }
/* The address breaks where the markup says (after the @) and nowhere else;
   the wrap-anywhere rule the tiles share was cutting "gov.bd" in two. */
.otile__v--mail { overflow-wrap:normal; word-break:normal; }
.otile__go { font-size:12.5px; font-weight:700; color:var(--brand-700); margin-top:2px; }
.otile--act { transition:background .12s ease; }
.otile--act:hover { background:var(--brand-50); text-decoration:none; }
.otile--act:hover .otile__ic { background:var(--brand-600); color:#fff; }
@media (min-width:640px) { .office__grid { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
@media (min-width:1000px) { .office__grid { grid-template-columns:1.5fr 1.3fr 1.1fr 1.3fr; } }

.cgroup + .cgroup { margin-top:18px; }
.cgroup__t {
  display:flex; align-items:center; gap:7px; margin:0 0 9px;
  font-size:12.5px; font-weight:750; letter-spacing:.03em; text-transform:uppercase; color:var(--text-2);
}
.cgroup__t svg { color:var(--text-3); }
.cgroup__n {
  font-size:11px; font-weight:700; padding:1px 7px; border-radius:999px;
  background:var(--muted-soft); color:var(--text-3); letter-spacing:0;
}
.site__sec[data-tone] .cgroup__t svg { color:var(--tone); }
/* A grid rather than a column: three cards across on a desktop, one on a phone. */
.site__contacts { grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:10px; }
.wfilter + .cgroup { margin-top:2px; }

/* ---- how it works: one grid --------------------------------------------
   The steps, the code and the film. A landscape film goes full width under
   the other two; a portrait one stands beside them. */
.site__how { display:grid; gap:22px; }
.site__how .site__video { margin-top:0; }
@media (min-width:900px) {
  .site__how {
    grid-template-columns:1fr 1fr; gap:30px;
    grid-template-areas:"steps code" "film film";
  }
  .site__how-steps { grid-area:steps; }
  .site__how-code  { grid-area:code; }
  .site__how-film  { grid-area:film; }
  .site__how--tall {
    grid-template-columns:minmax(0, 1fr) 340px;
    /* The first row is as tall as the steps and no taller, so the code sits
       directly under them; the film spans both. The columns stretch to one
       height, and the film fills its column, so the three blocks share a top
       edge and a bottom edge instead of ending wherever each happened to. */
    grid-template-rows:max-content 1fr;
    grid-template-areas:"steps film" "code film";
    align-items:stretch;
  }
  .site__how--tall .site__how-film { display:flex; flex-direction:column; }
  .site__how--tall .vplay--tall { max-width:none; aspect-ratio:auto; flex:1 1 auto; min-height:420px; margin-top:12px; }
  .site__how--tall .site__how-code { align-self:end; }
}

/* ---- the ward filter ----
   Nine cards is a list somebody scans; it is not a list they should scan when
   they already know which ward they live in. */
.wfilter { margin:0 0 14px; }
.wfilter__lbl {
  display:block; font-size:11.5px; font-weight:700; color:var(--text-3);
  letter-spacing:.03em; text-transform:uppercase; margin-bottom:6px;
}
.wfilter__row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.wfilter__row select { min-width:220px; max-width:100%; }
.wfilter__none {
  margin:0 0 4px; padding:14px; border-radius:var(--radius);
  background:var(--surface); border:1px dashed var(--line);
  font-size:13px; color:var(--text-3); text-align:center;
}

/* =====================================================================
   The public page, widened — and the panel that decides what is on it
   ===================================================================== */

/* ---- tones ----
   A hue attached to a subject, reused wherever that subject appears: the
   navigation card for "when is my collection" and the switch that hides it are
   the same blue, and the tab that edits the announcements is the amber the
   announcements card is.

   Built on the tokens already in :root rather than new hex — the semantic four
   where the meaning fits, and the categorical palette for the rest, which is
   the same set window.palette hands Chart.js. `--tone` is the name .kpi
   already uses for exactly this, so a reader who has met one has met both.

   Colour is never the only carrier. Every element that takes a tone also keeps
   its icon and its words; the hue is a second thing to recognise it by, not the
   thing itself. */
[data-tone="brand"]  { --tone:var(--brand-600); }
[data-tone="blue"]   { --tone:var(--info); }
[data-tone="green"]  { --tone:var(--ok); }
[data-tone="amber"]  { --tone:var(--warn); }
[data-tone="rose"]   { --tone:var(--bad); }
[data-tone="violet"] { --tone:var(--c4); }
[data-tone="teal"]   { --tone:var(--c6); }
[data-tone="indigo"] { --tone:var(--c2); }

/* The navigation grid under the hero. On a phone this is the whole of the
   navigation, read at arm's length in daylight by somebody who is not reading
   the labels so much as looking for the card they want. */
.gcard[data-tone] .gcard__ic { color:var(--tone); }
.gcard[data-tone] .gcard__ic {
  width:34px; height:34px; border-radius:9px; align-items:center; justify-content:center;
  background:color-mix(in srgb, var(--tone) 12%, transparent);
}
.gcard[data-tone]:hover { border-color:color-mix(in srgb, var(--tone) 45%, var(--line)); }

/* The editor's tabs, for the same reason: five grey glyphs in a row are five of
   the same thing, and whoever opens this screen is looking for the tab they
   were in last time. */
.ltab[data-tone] .ltab__ic { color:var(--tone); }
.ltab[data-tone]:hover { border-color:color-mix(in srgb, var(--tone) 45%, var(--line)); }
.ltab.is-on[data-tone] {
  background:var(--tone); border-color:var(--tone);
}
.ltab.is-on[data-tone] .ltab__ic { color:#fff; }

/* ---- the section switches ----
   One row per band of the public page, in the order a citizen meets them going
   down it. A list rather than a wrapped row of checkboxes: each of these needs
   a sentence saying what it hides, and a sentence does not fit in a chip. */
.secs { list-style:none; margin:0; padding:0; }
.secs__row {
  display:flex; align-items:flex-start; gap:12px;
  padding:11px 15px; border-bottom:1px solid var(--line);
}
.secs__row:last-child { border-bottom:0; }
.secs__ic {
  display:flex; align-items:center; justify-content:center; flex:0 0 auto;
  width:30px; height:30px; border-radius:8px; color:var(--tone, var(--brand-600));
  background:color-mix(in srgb, var(--tone, var(--brand-600)) 12%, transparent);
}
.secs__text { flex:1 1 auto; min-width:0; }
.secs__t { font-size:13.5px; font-weight:650; }
.secs__p { font-size:11.5px; color:var(--text-3); line-height:1.45; margin:2px 0 0; }
.secs__row .switch { margin-top:4px; }

/* ---- the picture strip ----
   Under the hero, full width of the shell. A fixed aspect ratio, because the
   photographs a municipality uploads are whatever its phone took and a band
   that changes height on every slide is a page that jumps while it is read. */
/* Inside .site__body, so it shares the shell's column and its side padding —
   this is a band in the page, not a full-bleed one. */
.site__slider { position:relative; margin:14px 0 0; }
.slider__track { list-style:none; margin:0; padding:0; position:relative; }
.slider__slide { display:none; }
.slider__slide.is-on { display:block; }
.slider__in {
  position:relative; display:block; overflow:hidden;
  border-radius:var(--radius); background:var(--muted-soft);
  aspect-ratio:21 / 9;
}
a.slider__in:hover { text-decoration:none; }
.slider__img { width:100%; height:100%; object-fit:cover; display:block; }
/* The caption sits on the picture, over a gradient rather than a flat panel:
   a municipal photograph is usually brightest in the middle and darkest at the
   edges, and a solid bar would cover the thing being shown. */
.slider__cap {
  position:absolute; inset:auto 0 0 0; padding:26px 18px 14px; color:#fff;
  background:linear-gradient(to top, rgb(3 26 22 / .82), rgb(3 26 22 / .45) 55%, transparent);
}
.slider__cap strong { display:block; font-size:16px; font-weight:750; line-height:1.3; }
.slider__cap span { display:block; font-size:12.5px; opacity:.92; margin-top:3px; line-height:1.45; }
.slider__arm {
  position:absolute; top:50%; transform:translateY(-50%);
  width:38px; height:38px; border-radius:50%; border:0; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  background:rgb(255 255 255 / .9); color:var(--text); box-shadow:var(--shadow);
}
.slider__arm:hover { background:#fff; }
.slider__arm:focus-visible { outline:2px solid var(--brand-500); outline-offset:2px; }
.slider__arm--prev { left:12px; }
.slider__arm--next { right:12px; }
[dir="rtl"] .slider__arm--prev { left:auto; right:12px; }
[dir="rtl"] .slider__arm--next { right:auto; left:12px; }
.slider__dots {
  position:absolute; left:0; right:0; bottom:10px;
  display:flex; gap:6px; justify-content:center;
}
.slider__dot {
  width:8px; height:8px; padding:0; border:0; border-radius:50%; cursor:pointer;
  background:rgb(255 255 255 / .55); box-shadow:0 0 0 1px rgb(3 26 22 / .25);
}
.slider__dot.is-on { background:#fff; width:20px; border-radius:999px; }
.slider__dot:focus-visible { outline:2px solid #fff; outline-offset:2px; }
@media (max-width:640px) {
  .slider__in { aspect-ratio:4 / 3; }
  /* The arrows are a desktop affordance. On a phone the strip is swiped, and
     two floating circles over a photograph this size are two things covering
     the picture they are there to show. */
  .slider__arm { display:none; }
  .slider__cap strong { font-size:14px; }
}

/* ---- the film ----
   A facade: YouTube's own still with a play button over it, and the iframe is
   not written into the page until somebody presses it. */
.site__video { margin-top:26px; }
.vplay {
  position:relative; overflow:hidden; border-radius:var(--radius);
  background:#0b1220; aspect-ratio:16 / 9; margin-top:10px;
}
.vplay__still { width:100%; height:100%; object-fit:cover; display:block; opacity:.92; }
.vplay__go {
  position:absolute; inset:0; width:100%; height:100%; border:0; cursor:pointer;
  background:transparent; display:flex; align-items:center; justify-content:center;
}
.vplay__tri {
  width:64px; height:64px; border-radius:50%; background:rgb(255 255 255 / .92);
  box-shadow:0 6px 24px rgb(3 26 22 / .35);
  display:block; position:relative; transition:transform .14s ease, background .14s ease;
}
.vplay__tri::after {
  content:""; position:absolute; top:50%; left:54%; transform:translate(-50%,-50%);
  border-style:solid; border-width:11px 0 11px 18px;
  border-color:transparent transparent transparent var(--brand-700);
}
.vplay__go:hover .vplay__tri { transform:scale(1.06); background:#fff; }
.vplay__go:focus-visible { outline:2px solid var(--brand-500); outline-offset:-4px; }
/* A Short: portrait, so drawn upright and no wider than a phone held the way
   it was filmed. Centred in the column, with the page's own tint either side
   rather than YouTube's black. */
.vplay--tall { aspect-ratio:9 / 16; max-width:360px; margin-inline:auto; }
.vplay__frame { width:100%; height:100%; border:0; display:block; }
.vplay__file { width:100%; border-radius:var(--radius); display:block; background:#0b1220; }
.vplay__out { position:absolute; left:50%; bottom:14px; transform:translateX(-50%); }

/* ---- the office on a map ----
   The same facade as the film: a card this page drew, and Google's frame only
   once somebody has asked for it. The card is the height of what it says —
   where, and the two things to do about it — and only the live frame is the
   height of a map. It used to be the frame's full height with a pin floating
   in the middle, which on a phone was most of a screen of nothing. */
.gmap { margin:22px 0 0; }
.gmap__frame {
  position:relative; overflow:hidden; border-radius:16px;
  border:1px solid var(--line-strong); background:var(--surface);
  box-shadow:0 8px 24px rgb(15 23 42 / .06);
}
.gmap__frame.is-live { height:clamp(260px, 40vw, 400px); }
.gmap__facade {
  display:flex; flex-direction:column; gap:14px; padding:16px;
  background:
    radial-gradient(circle at 85% 20%, rgb(255 255 255 / .55), transparent 50%),
    linear-gradient(140deg, var(--brand-50), var(--muted-soft));
}
.gmap__pin {
  width:44px; height:44px; flex:0 0 44px; border-radius:12px; display:grid; place-items:center;
  background:var(--surface); color:var(--brand-700);
  box-shadow:0 4px 14px rgb(3 26 22 / .14);
}
.gmap__text { min-width:0; flex:1 1 auto; }
.gmap__t {
  margin:0 0 2px; font-size:11.5px; font-weight:750; letter-spacing:.06em;
  text-transform:uppercase; color:var(--text-3);
}
.lang-bn .gmap__t { letter-spacing:0; font-size:12px; }
.gmap__where { margin:0; font-size:15.5px; font-weight:750; color:var(--text-1); }
.gmap__note { margin:3px 0 0; font-size:13px; line-height:1.5; color:var(--text-2); }
.gmap__priv { margin:6px 0 0; font-size:12px; color:var(--text-3); }
/* Full-size buttons, side by side, each at least a thumb wide: these were
   btn--sm — twenty-eight pixels tall on a page read on a phone, and the only
   controls in the band that were. */
.gmap__acts, .gmap__after { display:flex; flex-wrap:wrap; gap:8px; }
.gmap__acts .btn, .gmap__after .btn { flex:1 1 150px; min-height:42px; }
.gmap__after { margin-top:10px; }
.gmap__iframe { width:100%; height:100%; border:0; display:block; }
@media (min-width:640px) {
  .gmap__facade { flex-direction:row; align-items:center; padding:18px 20px; }
  .gmap__acts { flex:0 0 auto; }
  .gmap__acts .btn { flex:0 0 auto; }
}

/* ---- where else this municipality is ----
   A row of cards rather than a row of bare marks: a citizen who does not
   recognise a glyph still reads "UNO Teknaf, facebook.com", and the tap target
   is a card instead of a nineteen-pixel icon. Each carries its own colour, so
   the Facebook page is not the same grey as the district portal. */
.slinks { margin:22px 0 0; }
.slinks__t {
  display:flex; align-items:center; gap:7px; margin:0 0 8px;
  font-size:12.5px; font-weight:750; letter-spacing:.03em; text-transform:uppercase; color:var(--text-2);
}
.lang-bn .slinks__t { letter-spacing:0; }
.site__sec[data-tone] .slinks__t svg { color:var(--tone); }
.slinks__row {
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(auto-fill, minmax(230px, 1fr)); gap:10px;
}
.slink {
  display:flex; align-items:center; gap:11px; padding:11px 14px; min-width:0;
  border-radius:14px; background:var(--surface); border:1px solid var(--line-strong);
  color:inherit; text-decoration:none; transition:border-color .12s ease, transform .12s ease;
}
.slink:hover { text-decoration:none; transform:translateY(-1px); border-color:var(--kind, var(--brand-600)); }
.slink__ic {
  width:38px; height:38px; flex:0 0 38px; border-radius:11px; display:grid; place-items:center;
  background:color-mix(in srgb, var(--kind, var(--brand-600)) 12%, transparent);
  color:var(--kind, var(--brand-700));
}
.slink__text { min-width:0; display:flex; flex-direction:column; }
.slink__l { font-size:14px; font-weight:700; color:var(--text-1); overflow-wrap:anywhere; }
.slink__h { font-size:11.5px; color:var(--text-3); overflow-wrap:anywhere; }
/* The marks' own colours, which are what the eye finds the row by. Set on the
   link so both the tile and the hover border read from one place. */
.slink[data-kind="facebook"]  { --kind:#1877f2; }
.slink[data-kind="youtube"]   { --kind:#ff0033; }
.slink[data-kind="instagram"] { --kind:#c13584; }
.slink[data-kind="whatsapp"]  { --kind:#128c7e; }
.slink[data-kind="twitter"]   { --kind:#111827; }
.slink[data-kind="linkedin"]  { --kind:#0a66c2; }
.slink[data-kind="telegram"]  { --kind:#229ed9; }
[data-theme="dark"] .slink[data-kind="twitter"] { --kind:#e5e7eb; }
@media (prefers-color-scheme:dark) {
  :root:not([data-theme="light"]) .slink[data-kind="twitter"] { --kind:#e5e7eb; }
}

/* The same links at the foot of every page on the site: marks only, because
   the row above has already said what each one is. */
.site__foot-links { list-style:none; display:flex; flex-wrap:wrap; gap:8px; margin:3px 0 0; padding:0; }
.site__foot-links a {
  width:34px; height:34px; border-radius:10px; display:grid; place-items:center;
  background:rgba(255,255,255,.12); color:#fff; text-decoration:none;
  transition:background .12s ease;
}
.site__foot-links a:hover { background:rgba(255,255,255,.26); color:#fff; }


/* ---- a contact card, with a face and two ways to reach them ---- */

/* An address is longer than a number and must be allowed to break, or one long
   one pushes the card wider than the column it sits in. */
.land__mail { max-width:100%; }
.land__mail-a { overflow-wrap:anywhere; }
.land-ed__thumb--face { border-radius:50%; }

/* ---- carry on from a phone ----
   The code on the complaint form. Quiet and at the end: it is an offer to
   somebody who has found they cannot finish here, not a thing to read first. */
.report__qr {
  display:flex; gap:14px; align-items:center; margin:22px 0 4px;
  padding:14px; border:1px solid var(--line); border-radius:var(--radius);
  background:var(--surface-2);
}
.report__qr-code { width:104px; height:104px; flex:0 0 auto; background:#fff; border-radius:6px; padding:5px; }
.report__qr-code svg { width:100%; height:100%; display:block; }
.report__qr-t { font-size:13.5px; font-weight:700; }
.report__qr-p { font-size:12px; color:var(--text-2); margin:3px 0 6px; line-height:1.5; }
.report__qr-url { font-size:11px; color:var(--text-3); overflow-wrap:anywhere; }
/* Not drawn on a phone at all: a code offering to move this page to a phone,
   shown on the phone it would move it to, is furniture. Nor on paper, where it
   would be a code pointing at the page somebody has just printed. */
@media (max-width:720px) { .report__qr { display:none; } }
@media print { .report__qr { display:none; } }

@media (prefers-reduced-motion: reduce) {
  .vplay__go:hover .vplay__tri { transform:none; }
}

/* =====================================================================
   The citizens' page: a proportion pass
   ---------------------------------------------------------------------
   Last in the file on purpose, because the page's rules above were written
   in layers and a few of the later layers undid the earlier ones. Each block
   says what it corrects.
   ===================================================================== */

/* ---- the hero and the grid over its foot ----
   The cards were pulled up over the hero by 14px, which read as a row that
   had slipped rather than one that floats on purpose. A real overlap, and
   the hero is given the room for it. */
.site__hero-in { padding-bottom:46px; }
.site__grid { margin-top:-26px; }
.gcard { box-shadow:0 10px 28px rgb(15 23 42 / .08); }
.gcard:hover { box-shadow:0 14px 34px rgb(15 23 42 / .12); }
/* The line under each card title was the faintest grey on the page, on the
   one row that is the whole of the navigation on a phone. */
.gcard__p { color:var(--text-2); }
@media (min-width:900px) {
  .site__hero-in { padding-block:48px 84px; }
  .site__grid { margin-top:-44px; }
}
.site__hero-figs small { font-size:12px; opacity:.9; }
.site__hero-figs li { backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }

/* The two buttons were each as wide as their words, so on a phone and a
   tablet they stacked as two different widths. Below the desktop they share
   the row, or each takes all of it. */
@media (max-width:1099px) {
  .site__hero-acts .site__cta { flex:1 1 auto; }
}
/* "Already reported? Track it" carried a left margin meant for sitting beside
   the sentence; when it wrapped under it, the margin became an indent. */
.site__hero-note { display:flex; flex-wrap:wrap; align-items:baseline; column-gap:12px; row-gap:4px; }
.site__hero-track { margin-inline-start:0; }

/* ---- the picture strip ----
   The dots sat on the caption's last line and cut through its words. The
   caption now keeps a lane clear for them, and the strip stops growing at
   a height that leaves the page below it on a laptop screen. */
.slider__in { max-height:440px; width:100%; }
.site__slider:has(.slider__dots) .slider__cap { padding-bottom:34px; }
.slider__cap { padding-inline:20px; }
.slider__cap strong, .slider__cap span { max-width:72ch; }
.slider__dots { bottom:12px; }

/* ---- track your report ----
   `.track__form` is declared after `.track__form--panel` with the same
   weight, so its 640px cap won and the box stopped two thirds of the way
   across its own panel. */
.track__form.track__form--panel { max-width:none; flex-wrap:nowrap; }
.track__form--panel input.track__q { flex:1 1 auto; }
@media (max-width:640px) {
  .track__form.track__form--panel { flex-wrap:wrap; }
  /* The panel's icon took a row of its own on a phone to repeat the
     magnifier already on the button. */
  .tpanel__ic { display:none; }
}

/* ---- the ward filter ----
   Select2 writes width:100% onto its own box, so a list of nine wards was a
   control the width of the page. */
.wfilter__row .select2-container { width:min(100%, 360px) !important; }

/* ---- section rhythm ----
   42px above each section and 10px below it left the last line of every
   band touching the rule under it. Even space, top and bottom. */
.site__sec { padding-block:34px 30px; }
@media (min-width:900px) { .site__sec { padding-block:52px 44px; } }
.site__sec .site__aside:last-child { margin-bottom:0; }

/* ---- announcements ----
   A thick left edge on a card with a photograph across its top inset the
   photograph by three pixels on one side only. The hue is a strip along the
   top instead, which the photograph sits under cleanly. */
.ncard[data-tone], .ncard.is-pinned { border-inline-start:1px solid var(--line-strong); }
.ncard[data-tone]:hover { border-color:color-mix(in srgb, var(--tone) 45%, var(--line)); }
.ncard[data-tone]::before {
  content:""; position:absolute; inset:0 0 auto 0; height:3px; z-index:1;
  background:color-mix(in srgb, var(--tone) 70%, #fff);
}
.ncard[data-tone]:hover::before { background:var(--tone); }
.site__news { align-items:stretch; }
.ncard__text { display:flex; flex-direction:column; flex:1 1 auto; }
.ncard__go { margin-top:auto; padding-top:9px; align-self:flex-start; }

/* ---- the film on a phone ----
   A portrait film at the full width of a phone is taller than the phone. */
@media (max-width:899px) {
  .vplay--tall { max-width:300px; }
}

/* ==== form rules: the summary strip, the default tag ====================
   One line per rule, as it stands, above the cards that change it. A chip
   says the setting in the same word the option does; "not asked" is struck
   through so a question the form has stopped asking reads as gone, and one
   that is insisted on is filled. */
.fr-sum__list {
  list-style:none; margin:0; padding:12px 16px 14px;
  display:grid; gap:8px; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));
}
.fr-sum__item {
  display:flex; flex-direction:column; gap:2px; min-width:0;
  padding:9px 11px; border-radius:10px;
  border:1px solid var(--line); background:var(--surface-2);
  transition:background .12s ease, border-color .12s ease;
}
.fr-sum__l { font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--text-3); }
.fr-sum__v { font-size:13.5px; font-weight:700; color:var(--text-1); overflow-wrap:anywhere; }
.fr-sum__item[data-v="required"] { background:var(--brand-50); border-color:color-mix(in srgb, var(--brand-600) 40%, var(--line)); }
.fr-sum__item[data-v="required"] .fr-sum__v { color:var(--brand-700); }
.fr-sum__item[data-v="hidden"] .fr-sum__v { color:var(--text-3); text-decoration:line-through; font-weight:600; }
.fr-sum__item.is-changed { box-shadow:inset 0 0 0 1.5px var(--warn); }

.fr-opt__def {
  display:inline-block; margin-inline-start:6px; padding:1px 7px; border-radius:999px;
  font-size:10px; font-weight:700; letter-spacing:.03em; vertical-align:1px;
  background:var(--muted-soft); color:var(--text-2);
}
.fr-opt.is-on .fr-opt__def { background:#fff; color:var(--brand-700); }
[data-fr-defaults] { display:inline-flex; align-items:center; gap:6px; }

/* The citizen's form, when a photograph is insisted on and none was added. */
.report__form [data-photo-err] {
  margin:10px 0 0; padding:9px 12px; border-radius:10px;
  background:var(--bad-soft); color:var(--bad); font-size:13.5px; font-weight:600;
}

/* ==== delete controls on the registers (views/partials/delete-button.ejs) ====
   Red at rest. They were grey until pointed at, on the idea that a column of
   red bins would shout over the record; in use the grey bin sat beside the
   grey edit and view icons and people hunted for it, or pressed the wrong
   one. Now the bin is the one red thing in the row — a red glyph on white
   with a pale red edge, which is findable without being loud — and it fills
   solid red under the pointer or the keyboard, the moment before the confirm.

   A delete that is refused because the record is still in use is drawn
   disabled: grey, dimmed, no hover — "cannot" must not look like "can". */
.del-form { display:inline-flex; margin:0; }
/* `:is(.tbl, body)` lifts these to the weight of `.tbl .btn--sm.btn--ghost`,
   which paints every small ghost button in a table the brand green — it was
   winning, so in most registers the bin was green, not grey and not red.
   Declared after it, these now win inside a table and out of one. */
:is(.tbl, body) .btn.btn--del {
  color:var(--bad); padding-inline:8px; background:var(--surface);
  border-color:color-mix(in srgb, var(--bad) 30%, var(--line));
}
:is(.tbl, body) .btn.btn--del:hover, :is(.tbl, body) .btn.btn--del:focus-visible {
  color:#fff; background:var(--bad); border-color:var(--bad);
}
:is(.tbl, body) .btn.btn--del:focus-visible { outline:none; box-shadow:0 0 0 3px color-mix(in srgb, var(--bad) 25%, transparent); }
:is(.tbl, body) .btn.btn--del:disabled,
:is(.tbl, body) .btn.btn--del:disabled:hover {
  color:var(--text-3); background:var(--surface-2); border-color:var(--line);
  opacity:.7; cursor:not-allowed; box-shadow:none;
}
.btn--danger-ghost { color:var(--bad); border-color:color-mix(in srgb, var(--bad) 35%, var(--line)); background:var(--surface); display:inline-flex; align-items:center; gap:6px; }
.btn--danger-ghost:hover, .btn--danger-ghost:focus-visible { background:var(--bad); border-color:var(--bad); color:#fff; }
/* The solid delete, where it keeps its words: the bin sits beside them. */
.btn--danger { display:inline-flex; align-items:center; justify-content:center; gap:6px; }
.btn--danger svg, .btn--danger-ghost svg, .btn--del svg { flex:0 0 auto; }

/* ==== form rules, compact (views/admin/form_rules.ejs) ====================
   Eight rules drawn as full-width cards of three explained options each ran
   past two thousand pixels. Now a rule is one band: what it is on the left,
   the choice on the right as a slim segmented control, and of the three
   consequences only the chosen one is written out, under the control. The
   reasoning stays, clamped to two lines with a More toggle. */
.fr-rule { padding:12px 16px; display:grid; gap:10px 24px; grid-template-columns:minmax(0,1fr); }
.fr-rule__head { margin:0; display:block; }
.fr-rule__text { min-width:0; }
.fr-rule__name { font-size:13.5px; }
.fr-rule__where { flex:none; margin-top:3px; gap:4px; font-size:10.5px; }
.fr-rule__where code { font-size:10.5px; padding:0 5px; }
.fr-rule__hint {
  margin-top:5px; font-size:12px; line-height:1.5; max-width:62ch;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.fr-rule__hint.is-open { display:block; -webkit-line-clamp:unset; overflow:visible; }
.fr-more { border:0; background:none; padding:0; margin-top:2px; font-size:11.5px; font-weight:650; color:var(--brand-700); cursor:pointer; }
.fr-more:hover { text-decoration:underline; }

.fr-rule__ctl { min-width:0; align-self:center; }
.fr-rule--choice .fr-opts {
  display:inline-flex; flex-wrap:wrap; gap:0; max-width:100%;
  border:1px solid var(--line-strong); border-radius:10px; overflow:hidden; background:var(--surface);
}
.fr-rule--choice .fr-opt {
  flex:1 1 auto; align-items:center; gap:7px; padding:7px 13px; border:0; border-radius:0;
  background:transparent; min-height:34px;
}
.fr-rule--choice .fr-opt + .fr-opt { border-inline-start:1px solid var(--line); }
.fr-rule--choice .fr-opt:hover { background:var(--surface-2); }
.fr-rule--choice .fr-opt.is-on { background:var(--brand-600); }
.fr-rule--choice .fr-opt.is-on .fr-opt__name { color:#fff; }
.fr-rule--choice .fr-opt.is-on .fr-opt__mark { border-color:#fff; box-shadow:inset 0 0 0 3px var(--brand-600), inset 0 0 0 8px #fff; }
.fr-rule--choice .fr-opt__mark { width:14px; height:14px; margin:0; }
.fr-rule--choice .fr-opt__name { font-size:12.5px; white-space:nowrap; }
.fr-rule--choice .fr-opt.is-on .fr-opt__def { background:rgba(255,255,255,.22); color:#fff; }
/* The per-option sentences stay in the labels for a screen reader; the eye
   reads the one under the control. */
.fr-rule--choice .fr-opt__hint {
  position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
.fr-rule__chosen { margin:6px 2px 0; font-size:11.5px; line-height:1.45; color:var(--text-3); }
.fr-rule--path .fr-path { max-width:none; }
@media (min-width:900px) {
  .fr-rule { grid-template-columns:minmax(0,1fr) minmax(300px, auto); align-items:start; }
  .fr-rule__ctl { justify-self:end; max-width:560px; }
  .fr-rule--choice .fr-opts { flex-wrap:nowrap; }
  /* Seven chips in one row, not six and an orphan. */
  .fr-sum__list { grid-template-columns:repeat(auto-fill, minmax(118px, 1fr)); }
  .fr-rule__chosen { text-align:right; }
  .fr-rule--path { grid-template-columns:minmax(0,1fr) minmax(300px, 440px); }
}
@media (max-width:520px) {
  /* Three equal columns on a phone, never two rows of two and one. */
  .fr-rule--choice .fr-opts { display:grid; width:100%; grid-template-columns:none; grid-auto-flow:column; grid-auto-columns:minmax(0,1fr); }
  .fr-rule--choice .fr-opt { flex-direction:column; justify-content:center; gap:3px; padding:8px 4px; text-align:center; }
  .fr-rule--choice .fr-opt__name { white-space:normal; font-size:12px; line-height:1.25; }
  .fr-rule--choice .fr-opt__def { display:block; width:max-content; margin:3px auto 0; }
}

/* ==== form rules, touch-up (views/admin/form_rules.ejs) ===================
   The page is mostly sentences, and every sentence was drawn at nearly the
   same weight — the rule's name, its reasoning, the forms it reaches and what
   the chosen option does all competed. Now there is one order to read in:
   the name, the reasoning in a quieter voice, then where it applies as a
   small footnote; on the right an aligned column of controls, each with its
   consequence in a panel that takes the colour of the choice. Nothing about
   what the page does changes — this is the presentation of the same markup. */

/* The note above the page: a margin note, not a banner. */
.notice.notice--info:has(+ .fr-sum) {
  background:var(--surface); border-color:var(--line);
  border-inline-start:3px solid var(--brand-600);
  padding:10px 14px; font-size:12.5px; line-height:1.65; color:var(--text-2);
}
.notice.notice--info:has(+ .fr-sum) > span:first-child { color:var(--brand-600); margin-top:1px; }

/* The strip: quiet tiles, and the setting marked by a dot as well as a word. */
.fr-sum .card__head, #formRules .card__head { gap:8px 10px; }
.fr-sum__list { gap:8px; padding:12px 16px 16px; }
.fr-sum__item { background:var(--surface); padding:8px 11px 9px; gap:3px; }
.fr-sum__l { letter-spacing:.02em; text-transform:none; font-weight:650; font-size:11px; }
.fr-sum__v { display:flex; align-items:center; gap:6px; font-size:13px; line-height:1.3; }
.fr-sum__v::before {
  content:""; flex:0 0 auto; width:7px; height:7px; border-radius:50%;
  background:var(--line-strong);
}
.fr-sum__item[data-v="required"] .fr-sum__v::before { background:var(--brand-600); }
.fr-sum__item[data-v="optional"] .fr-sum__v::before { background:var(--info, #0284c7); }
.fr-sum__item[data-v="hidden"] .fr-sum__v::before { background:transparent; box-shadow:inset 0 0 0 1.5px var(--text-3); }

/* A rule: the text in one column, reading top to bottom. */
#formRules .fr-rule { padding:16px 18px; gap:12px 32px; transition:background .12s ease; }
#formRules .fr-rule:hover { background:color-mix(in srgb, var(--surface-2) 55%, transparent); }
#formRules .fr-rule.is-changed { box-shadow:inset 3px 0 0 var(--warn); }
#formRules .fr-rule__text { display:flex; flex-direction:column; align-items:flex-start; }
#formRules .fr-rule__name { order:1; font-size:14px; font-weight:700; line-height:1.35; color:var(--text); }
#formRules .fr-rule__hint {
  order:2; margin:5px 0 0; font-size:12.5px; line-height:1.6; color:var(--text-2); max-width:64ch;
}
#formRules .fr-more { order:3; margin-top:3px; }
/* Where it applies: a footnote under the reasoning, not a line above it. */
#formRules .fr-rule__where { order:4; margin-top:8px; opacity:.85; }
#formRules .fr-rule__where-label { font-weight:650; }
#formRules .fr-rule__where code { background:var(--surface-2); border-color:var(--line); color:var(--text-3); }

/* What the chosen option does, as a small panel under the control, tinted by
   the choice: filled for insisted on, plain for asked, dashed for not asked. */
#formRules .fr-rule__chosen {
  margin:8px 0 0; padding:7px 10px; border-radius:8px; text-align:start;
  font-size:12px; line-height:1.5; color:var(--text-2);
  background:var(--surface-2); border:1px solid var(--line);
  border-inline-start:3px solid var(--line-strong);
}
#formRules .fr-rule:has(input[value="required"]:checked) .fr-rule__chosen {
  background:var(--brand-50); border-color:color-mix(in srgb, var(--brand-600) 25%, var(--line));
  border-inline-start-color:var(--brand-600); color:var(--text);
}
#formRules .fr-rule:has(input[value="hidden"]:checked) .fr-rule__chosen {
  background:transparent; border-style:dashed; border-inline-start-style:solid;
}

/* The path rule's box and its notes, at the same rhythm. */
#formRules .fr-path__note { font-size:12px; line-height:1.55; }
#formRules .fr-path__kept { font-size:12px; line-height:1.9; }

@media (min-width:900px) {
  /* One column of controls, aligned from rule to rule, each option an equal
     third — the eye runs straight down it instead of zig-zagging. */
  #formRules .fr-rule--choice { grid-template-columns:minmax(0,1fr) minmax(360px, 440px); }
  #formRules .fr-rule--choice .fr-rule__ctl { justify-self:stretch; max-width:none; }
  #formRules .fr-rule--choice .fr-opts {
    /* The base rule's 210px template would fit two and squeeze the third. */
    display:grid; width:100%; grid-template-columns:none;
    grid-auto-flow:column; grid-auto-columns:minmax(0,1fr);
  }
  #formRules .fr-rule--choice .fr-opt { justify-content:center; padding:8px 10px; }
  #formRules .fr-rule__chosen { text-align:start; }
  #formRules .fr-rule--path { grid-template-columns:minmax(0,1fr) minmax(360px, 440px); }
}
@media (max-width:520px) {
  #formRules .fr-rule { padding:14px; }
  #formRules .fr-rule__where { margin-top:6px; }
}

/* ==== a register row's actions, on one line (views/admin/users.ejs) =======
   The words stay on the two things done most (Edit, Permissions); the rarer
   ones — disable login, sign out everywhere, delete — are icons with their
   name in the tooltip and to a screen reader. Five labelled buttons wrapped a
   row to four lines. */
.row-acts { flex-wrap:wrap; gap:4px; align-items:center; max-width:200px; }
/* Edit and Permissions on the first line, the icons on the second: one line of
   five pushed the register wider than its card and hid the status column. */
.row-acts > .btn:not(.btn--icon) { flex:0 0 auto; }
/* The cell keeps room for its two lines; without it the table squeezed the
   column to one button wide and stacked five of them. */
.tbl td:has(> .row-acts) { min-width:205px; }
.btn--icon { padding:0 8px; color:var(--text-3); }
.btn--icon:hover, .btn--icon:focus-visible { color:var(--text-1); }
.btn--icon.is-off { color:var(--ok); }

/* The status dialog's two selects side by side, stacked on a phone where two
   172px columns cut "— unassigned —" off mid-word. */
.sm-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
@media (max-width:560px) { .sm-grid { grid-template-columns:minmax(0,1fr); } }

/* ==== complaint photos, compact (status dialog and the complaint's page) ===
   The upload used to be a camera/gallery switch over a large dashed target,
   under two tall before/after cards — most of a dialog before anything was
   chosen. Now: a slim before/after switch, the plain camera + gallery button
   pair, and the pictures already on the complaint as a strip of thumbnails. */
.ba-pick--compact { gap:6px; margin-bottom:8px; }
.ba-pick--compact .ba-pick__box { min-height:0; padding:6px 10px; gap:7px; border-width:1px; }
.ba-pick--compact .ba-pick__icon { width:auto; height:auto; background:none; }
.ba-pick--compact .ba-pick__text em { display:none; }
.ba-pick--compact .ba-pick__text strong { font-size:13px; }

.sm-have { margin:0 0 10px; }
.sm-have__label { display:block; font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--text-3); margin-bottom:5px; }
.sm-have__row { display:flex; gap:6px; flex-wrap:wrap; }
.sm-have__pic { position:relative; width:64px; height:64px; border-radius:8px; overflow:hidden; border:1px solid var(--line); background:var(--surface-2); flex:0 0 auto; }
.sm-have__pic img { width:100%; height:100%; object-fit:cover; display:block; }
.sm-have__pic:hover { border-color:var(--brand-500); }
.sm-have__tag { position:absolute; left:3px; bottom:3px; padding:0 5px; border-radius:4px; font-size:9.5px; font-weight:700; line-height:15px; color:#fff; background:rgba(15,23,42,.72); }
.sm-have__tag--after { background:color-mix(in srgb, var(--ok) 88%, #000); }
.sm-have__tag--before { background:color-mix(in srgb, var(--warn) 88%, #000); }

/* The complaint page's upload row: the buttons and the send button on one line. */
.photo-add .photo-actions { margin-top:0; }
#photos .photo-add { gap:8px; }
/* On the complaint's page the before/after columns hold thumbnails, not
   posters: a single photograph used to stretch to the whole column. They open
   full size in the lightbox. */
#photos .photo-strip--gallery { grid-template-columns:repeat(auto-fill, minmax(96px, 128px)); gap:8px; }
#photos .photo-tile__bar { padding-top:3px; }
#photos .photo-add__go { margin-top:2px; }

/* ==== the photo area, aligned (status dialog + the complaint's page) ======
   One grid for both, so every row shares the same left and right edge:
     dialog:  label ........ [Before | After]      page:  [Before | After]
              already attached: ▢ ▢                        [Choose][Take]   [Add photos]
              [ Choose a photo ][ Take photo ]             nothing chosen yet
              nothing chosen yet
   Hidden inputs, the file inputs and the closed camera dialog are display:none,
   so they take no cell. */
.photo-add {
  display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:12px; row-gap:8px;
  align-items:center;
}
.photo-add > label { grid-column:1; margin:0; }
.photo-add > .ba-pick { grid-column:2; justify-self:end; }
.photo-add > .sm-have,
.photo-add > .photo-grid,
.photo-add > .field__note { grid-column:1 / -1; margin:0 !important; }
.photo-add > .btn-group {
  grid-column:1 / -1; margin:0 !important;
  display:grid; grid-template-columns:repeat(auto-fit, minmax(0,1fr)); gap:8px;
}
.photo-add > .btn-group > .btn { justify-content:center; }
.photo-add > .photo-grid:empty { display:none; }
.photo-add > .field__note { font-size:12px; color:var(--text-3); }

/* The before/after choice as one segmented control, the width of its words. */
.ba-pick--compact {
  display:inline-grid; grid-auto-flow:column; grid-template-columns:none; gap:0;
  margin:0; border:1px solid var(--line-strong); border-radius:10px; overflow:hidden; background:var(--surface);
}
.ba-pick--compact .ba-pick__box { border:0; border-radius:0; padding:6px 12px; min-height:32px; background:transparent; }
.ba-pick--compact .ba-pick__opt + .ba-pick__opt .ba-pick__box { border-inline-start:1px solid var(--line); }
.ba-pick--compact .ba-pick__icon { background:none !important; width:auto; height:auto; }
.ba-pick--compact .ba-pick__icon .icon { width:15px; height:15px; }

/* "Already attached" reads as a caption on its row, not a second heading. */
.sm-have { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.sm-have__label { margin:0; font-size:11.5px; font-weight:600; letter-spacing:0; text-transform:none; color:var(--text-2); }
.sm-have__pic { width:52px; height:52px; }

/* The page: no label, so the switch leads; Add photos sits at the end of the
   buttons' row instead of on a line of its own. */
#photos .photo-add { margin-top:14px; padding-top:14px; border-top:1px dashed var(--line); grid-template-columns:minmax(0,1fr) auto; }
#photos .photo-add > .ba-pick { grid-column:1 / -1; justify-self:start; }
#photos .photo-add > .btn-group { grid-column:1; }
#photos .photo-add > .photo-add__go { grid-column:2; grid-row:auto; margin:0; align-self:start; }
@media (max-width:560px) {
  .photo-add { grid-template-columns:minmax(0,1fr); }
  .photo-add > label, .photo-add > .ba-pick { grid-column:1; justify-self:stretch; }
  .ba-pick--compact { display:grid; grid-auto-columns:minmax(0,1fr); }
  #photos .photo-add > .btn-group, #photos .photo-add > .photo-add__go { grid-column:1; }
  #photos .photo-add > .photo-add__go { justify-self:stretch; }
}
/* Rows pinned, because the markup order is not the reading order: the switch
   comes after "already attached" in the dialog, and Add photos after the
   picker's grid and note on the page. */
.photo-add > label { grid-row:1; }
.photo-add > .ba-pick { grid-row:1; }
.photo-add > .sm-have { grid-row:2; }
.photo-add > .btn-group { grid-row:3; }
.photo-add > .photo-grid { grid-row:4; }
.photo-add > .field__note { grid-row:5; }
#photos .photo-add > .ba-pick { grid-row:1; }
#photos .photo-add > .btn-group, #photos .photo-add > .photo-add__go { grid-row:2; }
#photos .photo-add > .photo-grid { grid-row:3; }
#photos .photo-add > .field__note { grid-row:4; }
@media (max-width:560px) {
  .photo-add > .ba-pick { grid-row:2; }
  .photo-add > .sm-have { grid-row:3; }
  .photo-add > .btn-group { grid-row:4; }
  .photo-add > .photo-grid { grid-row:5; }
  .photo-add > .field__note { grid-row:6; }
  #photos .photo-add > .ba-pick { grid-row:1; }
  #photos .photo-add > .btn-group { grid-row:2; }
  #photos .photo-add > .photo-add__go { grid-row:3; }
  #photos .photo-add > .photo-grid { grid-row:4; }
  #photos .photo-add > .field__note { grid-row:5; }
}

/* ==== the photo area, the full controls again — aligned ==================
   The descriptive before/after cards and the camera | gallery switch over one
   target are back (they read better than the bare buttons), on the same grid
   as everything else so every row shares its edges:
     dialog: label / already attached / [Before][After] / switch / target / note
     page:   [Before][After] / switch / target / note ......... [Add photos]
   These rules come last and replace the row pinning above. */
.photo-add { grid-template-columns:minmax(0,1fr) auto; row-gap:10px; }
.photo-add > label           { grid-column:1 / -1; grid-row:1; }
.photo-add > .sm-have        { grid-column:1 / -1; grid-row:2; }
.photo-add > .ba-pick        { grid-column:1 / -1; grid-row:3; justify-self:stretch; margin:0; }
.photo-add > .pp-src,
.photo-add > .pp-only        { grid-column:1 / -1; grid-row:4; margin:0; }
.photo-add > .pp-drop        { grid-column:1 / -1; grid-row:5; margin:0; }
.photo-add > .photo-grid     { grid-column:1 / -1; grid-row:6; }
.photo-add > .field__note    { grid-column:1; grid-row:7; align-self:center; }
.photo-add > .photo-add__go  { grid-column:2; grid-row:7; margin:0; }

#photos .photo-add > .ba-pick     { grid-row:1; justify-self:stretch; }
#photos .photo-add > .pp-src,
#photos .photo-add > .pp-only     { grid-row:2; }
#photos .photo-add > .pp-drop     { grid-row:3; }
#photos .photo-add > .photo-grid  { grid-row:4; }
#photos .photo-add > .field__note,
#photos .photo-add > .photo-add__go { grid-row:5; }
#photos .photo-add > .photo-add__go { grid-column:2; }

/* The cards a little shorter than on the citizen's form, same anatomy. */
.photo-add .ba-pick { gap:8px; }
.photo-add .ba-pick__box { min-height:46px; padding:7px 11px; gap:9px; }
.photo-add .ba-pick__icon { width:28px; height:28px; flex-basis:28px; border-radius:8px; }
.photo-add .ba-pick__box strong { font-size:13px; }
.photo-add .ba-pick__box em { font-size:11px; }
.photo-add .pp-drop { width:100%; }

@media (max-width:560px) {
  .photo-add { grid-template-columns:minmax(0,1fr); }
  .photo-add > label, .photo-add > .sm-have, .photo-add > .ba-pick,
  .photo-add > .pp-src, .photo-add > .pp-only, .photo-add > .pp-drop,
  .photo-add > .photo-grid, .photo-add > .field__note { grid-column:1; }
  .photo-add > .ba-pick { grid-row:3; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); }
  .photo-add > .field__note { grid-row:7; }
  .photo-add > .photo-add__go, #photos .photo-add > .photo-add__go { grid-column:1; grid-row:8; justify-self:stretch; }
  #photos .photo-add > .ba-pick { grid-row:1; }
  #photos .photo-add > .field__note { grid-row:5; }
  #photos .photo-add > .photo-add__go { grid-row:6; }
}

/* =====================================================================
   The citizens' site on the municipal-portal pattern (docs/site.png)
   ---------------------------------------------------------------------
   Last in the file so it is the final word over the layers above. The
   header and footer rules are scoped to `.site`, because the announcement
   and tracking pages share them; everything else is scoped to
   `.site--portal`, which only the landing page carries, so those pages keep
   their own bodies.

   The pattern: a white header, a photograph hero with the search box in it,
   white action cards floating over the hero's foot, and sections that open
   with a coloured icon tile, a heading and a "see all" on the right.
   ===================================================================== */

/* ---- header: white, with the brand's green as the accent ---- */
.site .site__head {
  background:#fff; color:var(--text);
  border-bottom:1px solid var(--line);
  box-shadow:0 2px 14px rgb(15 23 42 / .06);
}
.site .site__head-in { padding-block:10px; }
.site .site__mark {
  width:40px; height:40px; flex-basis:40px; border-radius:12px;
  background:linear-gradient(135deg, var(--brand-600), var(--brand-800)); color:#fff;
}
.site .site__org { color:var(--brand-900); font-size:15px; font-weight:750; }
.site .site__sub { color:var(--text-2); opacity:1; }
.site .site__burger { background:var(--surface-2); border-color:var(--line); color:var(--text); }
.site .site__burger:hover { background:var(--brand-50); color:var(--brand-700); }
.site .site__head .langswitch--report {
  background:var(--brand-50); border-color:color-mix(in srgb, var(--brand-600) 30%, var(--line));
  color:var(--brand-700); font-weight:650;
}
.site .site__head .langswitch--report:hover { background:var(--brand-100); border-color:var(--brand-600); color:var(--brand-800); }
.site .site__head-cta { background:var(--brand-600); color:#fff; padding:8px 16px; }
.site .site__head-cta:hover { background:var(--brand-700); color:#fff; }
@media (min-width:900px) {
  .site .site__nav { gap:4px; margin-inline-start:auto; }
  .site .site__nav a {
    color:var(--text-2); padding:7px 12px; border-radius:999px; border-bottom:0;
    font-size:13.5px;
  }
  .site .site__nav a:hover { color:var(--brand-700); background:var(--brand-50); }
  .site .site__head-acts { margin-inline-start:14px; }
}

/* ---- footer: four columns on deep green ---- */
.site .site__foot { margin-top:48px; background:#063a30; }
.site .site__foot-in { gap:28px; padding-block:40px 30px; }
@media (min-width:760px) { .site .site__foot-in { grid-template-columns:1.5fr 1fr 1.2fr; } }
.site .site__foot-mark { width:42px; height:42px; flex-basis:42px; border-radius:12px; background:rgba(255,255,255,.14); }
.site .site__foot-org { font-size:16px; }
.site .site__foot-blurb { margin:14px 0 0; font-size:12.5px; line-height:1.7; color:rgba(255,255,255,.66); max-width:42ch; }
.site .site__foot-h {
  margin:0 0 6px; font-size:14px; font-weight:700; color:#fff; letter-spacing:0;
  padding-bottom:8px; position:relative;
}
.site .site__foot-h::after {
  content:''; position:absolute; left:0; bottom:0; width:28px; height:2px;
  border-radius:2px; background:var(--brand-500);
}
.site .site__foot-nav { gap:9px; }
.site .site__foot-nav a { font-size:13px; }
.site .site__foot-line {
  display:inline-flex; align-items:flex-start; gap:8px; font-size:13px;
  color:rgba(255,255,255,.78); text-decoration:none; overflow-wrap:anywhere;
}
.site .site__foot-line svg, .site .site__foot-tel svg { flex:0 0 auto; margin-top:2px; }
a.site__foot-line:hover { color:#fff; text-decoration:underline; }
.site .site__foot-links a {
  width:36px; height:36px; border-radius:50%;
  border:1px solid rgba(255,255,255,.3); background:transparent;
}
.site .site__foot-copy { font-size:11.5px; color:rgba(255,255,255,.55); white-space:nowrap; }

/* ---- the page ---- */
.site--portal { background:#f6f8f7; }
.site--portal .site__body { max-width:1200px; }
.site--portal .site__head-in,
.site--portal .site__hero-in,
.site--portal .site__foot-in,
.site--portal .site__foot-legal-in { max-width:1200px; }

/* ---- hero ---- */
.site--portal .site__hero-in { padding:40px 16px 92px; align-items:center; }
@media (min-width:900px) {
  .site--portal .site__hero { min-height:520px; display:flex; }
  .site--portal .site__hero-in { width:100%; padding-block:72px 128px; grid-template-columns:1.6fr 1fr; }
  /* Dark behind the words, the photograph clear on the right: the portal's
     hero is a picture of the place, not a green panel. */
  .site--portal .site__hero::after {
    background:linear-gradient(95deg,
      rgba(3,40,33,.90) 0%, rgba(4,48,40,.78) 38%, rgba(5,58,49,.35) 72%, rgba(5,58,49,.15) 100%);
  }
}
.site--portal .site__h1 {
  font-size:clamp(30px, 4.4vw, 50px); line-height:1.15; font-weight:800;
  letter-spacing:-.02em; text-wrap:balance; margin-bottom:12px;
}
.site--portal .site__lead { font-size:clamp(15px, 1.4vw, 17.5px); max-width:52ch; }
.site--portal .site__hero-note { margin-top:10px; }

/* The search box: a white bar with the green button inside its right end. */
.hsearch { margin-top:26px; max-width:640px; }
.hsearch__form {
  display:flex; align-items:center; gap:6px;
  background:#fff; border-radius:14px; padding:6px 6px 6px 16px;
  box-shadow:0 14px 34px rgb(0 0 0 / .22);
}
.hsearch__ic { display:flex; color:var(--text-3); flex:0 0 auto; }
.site--portal .hsearch__q {
  flex:1 1 auto; min-width:0; height:46px; border:0; background:transparent;
  font:inherit; font-size:15px; color:var(--text); padding:0 6px; box-shadow:none; outline:none;
}
.hsearch__go {
  flex:0 0 auto; display:inline-flex; align-items:center; gap:8px; height:46px;
  padding:0 18px; border:0; border-radius:10px; cursor:pointer;
  background:var(--brand-600); color:#fff; font:inherit; font-weight:700; font-size:14px;
}
.hsearch__go:hover { background:var(--brand-700); }
.hsearch__form:focus-within { box-shadow:0 14px 34px rgb(0 0 0 / .22), 0 0 0 3px color-mix(in srgb, var(--brand-500) 55%, transparent); }
.hsearch__go-ic { display:flex; }
.hsearch__note { margin:8px 2px 0; font-size:12.5px; color:rgba(255,255,255,.78); }
@media (max-width:520px) {
  .hsearch__go { padding:0 14px; }
  .hsearch__go-l { display:none; }
}

/* The "popular" chips under it. */
.hchips { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:18px; }
.hchips__l { font-size:13px; font-weight:700; color:#fff; margin-inline-end:2px; }
.site--portal .hchip {
  display:inline-block; padding:6px 14px; border-radius:999px; font-size:12.5px; font-weight:600;
  color:#fff; text-decoration:none;
  background:rgba(255,255,255,.14); border:1px solid rgba(255,255,255,.34);
  backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
}
.site--portal .hchip:hover { background:rgba(255,255,255,.26); color:#fff; }

/* ---- the action cards over the hero's foot ---- */
.site--portal .site__grid {
  margin-top:-56px; gap:14px; grid-template-columns:1fr;
}
@media (min-width:560px) { .site--portal .site__grid { grid-template-columns:repeat(2, 1fr); } }
@media (min-width:1000px) {
  .site--portal .site__grid { grid-template-columns:repeat(4, 1fr); margin-top:-72px; gap:18px; }
}
.site--portal .gcard,
.site--portal .gcard--go {
  display:flex; flex-direction:row; align-items:center; gap:14px;
  padding:18px; border-radius:16px; border:1px solid var(--line);
  background:#fff; color:var(--text);
  box-shadow:0 12px 30px rgb(15 23 42 / .09);
}
.site--portal .gcard:hover { border-color:color-mix(in srgb, var(--tone, var(--brand-600)) 45%, var(--line)); transform:translateY(-3px); }
.site--portal .gcard .gcard__ic,
.site--portal .gcard--go .gcard__ic {
  width:50px; height:50px; flex:0 0 50px; border-radius:14px;
  display:grid; place-items:center; color:#fff; background:var(--tone, var(--brand-600));
  box-shadow:0 6px 14px color-mix(in srgb, var(--tone, var(--brand-600)) 32%, transparent);
}
.site--portal .gcard__ic svg { width:24px; height:24px; }
.site--portal .gcard__text { display:flex; flex-direction:column; gap:3px; flex:1 1 auto; min-width:0; }
.site--portal .gcard__t { margin:0; font-size:15.5px; font-weight:750; color:var(--text); }
.site--portal .gcard__p, .site--portal .gcard--go .gcard__p { color:var(--text-2); font-size:12.5px; }
.gcard__chev {
  flex:0 0 auto; width:32px; height:32px; border-radius:50%;
  display:grid; place-items:center; color:var(--text-2);
  border:1px solid var(--line); background:#fff;
  transition:background .14s ease, color .14s ease, border-color .14s ease;
}
.site--portal .gcard:hover .gcard__chev { background:var(--tone, var(--brand-600)); border-color:var(--tone, var(--brand-600)); color:#fff; }

.site--portal .land__notice { margin-top:22px; border-radius:14px; }

/* ---- a panel: the white box every side-by-side block is drawn in ---- */
.ppanel {
  background:#fff; border:1px solid var(--line); border-radius:18px;
  padding:20px; box-shadow:0 4px 18px rgb(15 23 42 / .05); min-width:0;
}
.ppanel__head {
  display:flex; align-items:center; gap:11px;
  padding-bottom:14px; margin-bottom:14px; border-bottom:1px solid var(--line);
}
.ppanel__ic {
  width:38px; height:38px; flex:0 0 38px; border-radius:11px; display:grid; place-items:center;
  background:color-mix(in srgb, var(--tone, var(--brand-600)) 12%, #fff); color:var(--tone, var(--brand-600));
}
.ppanel__t { margin:0; font-size:17px; font-weight:750; }
.ppanel .site__note { font-size:13.5px; margin-bottom:12px; }

/* ---- the feature row: pictures and the three steps ---- */
.pfeature { display:grid; gap:20px; margin-top:32px; }
@media (min-width:900px) { .pfeature--two { grid-template-columns:1.35fr 1fr; align-items:stretch; } }
.site--portal .site__slider { margin:0; min-width:0; }
.site--portal .slider__in { border-radius:18px; }
@media (min-width:900px) {
  .pfeature--two .site__slider,
  .pfeature--two .slider__track,
  .pfeature--two .slider__slide.is-on { height:100%; }
  .pfeature--two .slider__in { aspect-ratio:auto; height:100%; min-height:340px; max-height:none; }
}
.psteps { display:flex; flex-direction:column; }
.psteps__list { list-style:none; margin:0; padding:0; flex:1 1 auto; }
.pstep {
  display:flex; align-items:flex-start; gap:12px; padding:12px 0;
  border-bottom:1px dashed var(--line);
}
.pstep:first-child { padding-top:2px; }
.pstep:last-child { border-bottom:0; }
.pstep__n {
  width:30px; height:30px; flex:0 0 30px; border-radius:50%; display:grid; place-items:center;
  background:var(--brand-50); color:var(--brand-700); font-weight:800; font-size:13.5px;
}
.pstep__text { flex:1 1 auto; min-width:0; }
.pstep__t { font-size:14.5px; font-weight:700; }
.pstep__p { margin:3px 0 0; font-size:12.5px; line-height:1.55; color:var(--text-2); }
.pstep__ic { display:flex; color:var(--text-3); margin-top:5px; }
.psteps__go { margin-top:14px; width:100%; justify-content:center; min-height:46px; border-radius:12px; }

/* ---- a section's head: icon tile, heading, and an action on the right ---- */
.site--portal .site__sec { padding:48px 0 0; border-top:0; background:none; }
.site--portal .site__sec + .site__sec { border-top:0; }
.phead { display:flex; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-bottom:20px; }
.phead__ic {
  width:50px; height:50px; flex:0 0 50px; border-radius:14px; display:grid; place-items:center;
  background:color-mix(in srgb, var(--tone, var(--brand-600)) 13%, #fff); color:var(--tone, var(--brand-600));
}
.phead__text { flex:1 1 260px; min-width:0; }
.site--portal .phead .site__h2 { margin:2px 0 3px; font-size:clamp(20px, 2.2vw, 24px); font-weight:800; }
.site--portal .phead .site__note { margin:0; font-size:14px; }
.phead__all, .phead__act { margin-inline-start:auto; align-self:center; }
.site--portal .phead__all {
  display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  font-size:13.5px; font-weight:700; color:var(--brand-700);
  padding:8px 14px; border-radius:999px; background:#fff; border:1px solid var(--line);
}
.site--portal .phead__all:hover { border-color:var(--brand-600); background:var(--brand-50); }
.site--portal .phead .wfilter { margin:0; min-width:220px; }
.site--portal .phead .wfilter__lbl { font-size:12px; }
@media (max-width:640px) {
  .phead__all, .phead__act { margin-inline-start:0; width:100%; }
  .site--portal .phead .wfilter { min-width:0; }
}

/* ---- collection times: the service grid ---- */
.site--portal .site__wards { gap:14px; }
@media (min-width:1100px) { .site--portal .site__wards { grid-template-columns:repeat(3, 1fr); } }
.site--portal .wcard,
.site--portal .site__sec[data-tone] .wcard {
  background:#fff; border:1px solid var(--line); border-radius:16px; padding:16px 18px;
  box-shadow:0 2px 10px rgb(15 23 42 / .04);
  transition:transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
.site--portal .wcard:hover { transform:translateY(-2px); box-shadow:0 12px 26px rgb(15 23 42 / .09); border-color:color-mix(in srgb, var(--tone, var(--brand-600)) 40%, var(--line)); }
.wcard__top { display:flex; align-items:center; gap:11px; }
.wcard__ic {
  width:38px; height:38px; flex:0 0 38px; border-radius:11px; display:grid; place-items:center;
  background:color-mix(in srgb, var(--tone, var(--info)) 12%, #fff); color:var(--tone, var(--info));
}
.site--portal .wcard__name { font-size:15px; font-weight:750; min-width:0; }
.site--portal .wcard__when { margin-top:12px; }

/* ---- what goes where: the wide banner ---- */
.pbanner {
  position:relative; overflow:hidden; border-radius:20px; isolation:isolate;
  background:linear-gradient(100deg, #e3f5ee 0%, #eef9f5 45%, #f4fbf8 100%);
  border:1px solid color-mix(in srgb, var(--brand-600) 16%, var(--line));
}
.pbanner__img { display:block; width:100%; height:170px; object-fit:cover; }
.pbanner__text { padding:22px 20px 24px; }
.pbanner__t { margin:0 0 8px; font-size:clamp(21px, 2.5vw, 28px); font-weight:800; color:var(--brand-900); line-height:1.2; }
.pbanner__p { margin:0 0 18px; font-size:14.5px; line-height:1.6; color:var(--text-2); max-width:46ch; }
.site--portal .pbanner__go {
  display:inline-flex; align-items:center; gap:8px; padding:11px 20px; border-radius:12px;
  background:var(--brand-700); color:#fff; font-weight:700; font-size:14px;
}
.site--portal .pbanner__go:hover { background:var(--brand-800); color:#fff; }
@media (min-width:760px) {
  .pbanner { min-height:260px; display:flex; align-items:center; }
  .pbanner__img {
    position:absolute; inset:0 0 0 auto; width:62%; height:100%; z-index:-1;
    -webkit-mask-image:linear-gradient(90deg, transparent 0%, #000 34%);
            mask-image:linear-gradient(90deg, transparent 0%, #000 34%);
  }
  .pbanner__text { padding:34px 40px; max-width:54%; }
}
.site--portal .site__types { margin-top:18px; gap:12px; }
@media (min-width:1100px) { .site--portal .site__types { grid-template-columns:repeat(4, 1fr); } }
.site--portal .tcard { background:#fff; border-radius:14px; }

/* ---- news: picture cards ---- */
.site--portal .site__news { gap:20px; }
@media (min-width:900px) { .site--portal .site__news { grid-template-columns:repeat(3, 1fr); } }
.site--portal .ncard {
  border-radius:16px; overflow:hidden; background:#fff;
  box-shadow:0 4px 18px rgb(15 23 42 / .06);
  transition:transform .14s ease, box-shadow .14s ease;
}
.site--portal .ncard:hover { transform:translateY(-3px); box-shadow:0 16px 34px rgb(15 23 42 / .11); }
.site--portal .ncard__img, .site--portal .ncard__plate { height:200px; }
.site--portal .ncard__text { padding:16px 18px 18px; }
.site--portal .ncard__t { font-size:16px; }

/* ---- the figures: a picture beside four tiles and the split ---- */
.pfigs { display:grid; gap:20px; }
@media (min-width:900px) { .pfigs { grid-template-columns:1fr 1.25fr; align-items:stretch; } }
.pfigs__img { display:block; width:100%; height:220px; object-fit:cover; border-radius:18px; }
@media (min-width:900px) { .pfigs__img { height:100%; min-height:300px; } }
.pstats { list-style:none; margin:0 0 18px; padding:0; display:grid; grid-template-columns:repeat(2, 1fr); gap:12px; }
.pstats li {
  padding:14px 16px; border-radius:14px;
  background:color-mix(in srgb, var(--tone, var(--brand-600)) 7%, #fff);
  border:1px solid color-mix(in srgb, var(--tone, var(--brand-600)) 16%, var(--line));
}
.pstats li.is-good { background:var(--ok-soft); border-color:color-mix(in srgb, var(--ok) 30%, var(--line)); }
.pstats__n { display:block; font-size:clamp(24px, 2.6vw, 30px); font-weight:800; line-height:1.1; color:var(--tone, var(--brand-600)); font-variant-numeric:tabular-nums; }
.pstats li.is-good .pstats__n { color:var(--ok); }
.pstats__l { display:block; margin-top:4px; font-size:12.5px; color:var(--text-2); line-height:1.4; }
.pfigs__panel .site__aside { margin-top:12px; }

/* ---- the three-column row ---- */
.ptri { display:grid; gap:20px; margin-top:48px; }
@media (min-width:760px) { .ptri { grid-template-columns:repeat(2, 1fr); } }
@media (min-width:1100px) { .ptri { grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); } }
.site--portal .pqr .site__qr { padding:0; border:0; background:none; flex-direction:column; align-items:center; text-align:center; }
.site--portal .pqr .site__qr-code { width:190px; height:190px; }
.site--portal .pqr .site__qr-text { align-items:center; flex-basis:auto; }
.site--portal .pqr .land__qr-url { white-space:normal; overflow-wrap:anywhere; }

/* ---- contacts, map and links: the same cards, on the portal's radius ---- */
.site--portal .office, .site--portal .gmap__frame, .site--portal .slinks { border-radius:18px; }
.site--portal .ccard { border-radius:16px; }

/* ---- the closer: the portal's banner, once more ---- */
.site--portal .site__closer {
  margin-top:52px; padding:40px 24px; border-radius:20px;
  background:linear-gradient(110deg, var(--brand-900), var(--brand-700) 55%, var(--brand-600));
}
.site--portal .site__closer-h { font-size:clamp(20px, 2.4vw, 26px); }
/* .track__form wraps and caps its width for the tracking page's own box; in
   the hero it is one bar. */
.site--portal .hsearch__form { flex-wrap:nowrap; max-width:none; }
.site--portal .hsearch__q { width:auto; }

/* ---- contacts: one grid of people cards ----
   White cards on the portal's radius, the face or initial in a soft tile of
   the card's hue, the ward and hours as two quiet lines, and the two ways to
   reach them as buttons pinned to the card's foot — so the buttons across a
   row line up whatever the names and hours above them wrap to. */
.pcontacts {
  list-style:none; margin:0; padding:0;
  display:grid; gap:16px; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr));
}
.pcard {
  --tone-c:var(--tone, var(--brand-600));
  display:flex; flex-direction:column; gap:14px; min-width:0;
  padding:20px; border-radius:18px; background:#fff;
  border:1px solid var(--line); box-shadow:0 4px 18px rgb(15 23 42 / .05);
  transition:transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
.pcard:hover {
  transform:translateY(-3px); box-shadow:0 16px 34px rgb(15 23 42 / .10);
  border-color:color-mix(in srgb, var(--tone-c) 35%, var(--line));
}
.pcard__top { display:flex; align-items:center; gap:14px; min-width:0; }
.pcard__pic {
  width:56px; height:56px; flex:0 0 56px; border-radius:16px; object-fit:cover; display:block;
}
.pcard__pic--none {
  display:grid; place-items:center; font-size:22px; font-weight:800; line-height:1;
  background:color-mix(in srgb, var(--tone-c) 13%, #fff); color:color-mix(in srgb, var(--tone-c) 80%, #000);
}
.pcard__who { min-width:0; }
.pcard__name { margin:0; font-size:16.5px; font-weight:750; line-height:1.3; letter-spacing:-.005em; }
.pcard__role { margin:3px 0 0; font-size:13px; line-height:1.45; color:var(--text-2); }
.pcard__facts {
  list-style:none; margin:0; padding:12px 0 0; border-top:1px dashed var(--line);
  display:flex; flex-direction:column; gap:7px;
}
.pcard__facts li { display:flex; align-items:flex-start; gap:8px; font-size:13px; line-height:1.45; color:var(--text-2); }
.pcard__facts svg { flex:0 0 auto; margin-top:2px; color:var(--text-3); }
.pcard__acts { display:flex; flex-direction:column; gap:8px; margin-top:auto; }
.pcard__call, .pcard__mail {
  display:flex; align-items:center; justify-content:center; gap:8px; min-width:0;
  height:44px; padding:0 14px; border-radius:12px; font-size:14px; font-weight:700;
  text-decoration:none; transition:background .14s ease, border-color .14s ease, color .14s ease;
}
.pcard__call svg, .pcard__mail svg { flex:0 0 auto; }
.pcard__call span, .pcard__mail span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
.site--portal .pcard__call { background:var(--brand-600); color:#fff; font-variant-numeric:tabular-nums; }
.site--portal .pcard__call:hover { background:var(--brand-700); color:#fff; }
.site--portal .pcard__mail { background:#fff; color:var(--text); border:1px solid var(--line-strong); font-weight:600; font-size:13.5px; }
.site--portal .pcard__mail:hover { border-color:var(--brand-600); color:var(--brand-700); background:var(--brand-50); }
.site--portal .ctool { margin:22px 0 16px; }

/* ---- the sub-heads inside a section, one size ----
   "Find us elsewhere" was 12px and took the section's red; it reads as a
   heading of the same family as the panels' now. */
.site--portal .slinks { margin-top:24px; }
.site--portal .slinks__t {
  display:flex; align-items:center; gap:8px; margin:0 0 12px;
  font-size:15px; font-weight:750; color:var(--text); text-transform:none; letter-spacing:0;
}
.site--portal .site__sec[data-tone] .slinks__t svg { color:var(--brand-600); }
.site--portal .office { margin-bottom:4px; }
.site--portal .gmap { margin-top:22px; }

/* ---- the three-column row: every panel starts on the same line ---- */
.site--portal .ptri .site__video { margin-top:0; }
.site--portal .ptri > .ppanel { display:flex; flex-direction:column; }
.site--portal .ptri .vplay--tall { width:100%; max-width:300px; }

/* ---- news: the archive search runs the width of the cards above it ---- */
.site--portal .site__more--search { justify-content:flex-start; flex-wrap:nowrap; margin-top:20px; }
.site--portal .site__more--search .nfilter__q { flex:1 1 auto; max-width:none; }
@media (max-width:640px) { .site--portal .site__more--search { flex-wrap:wrap; } .site--portal .site__more--search .site__more-btn { width:100%; justify-content:center; } }
/* On a phone the ward list takes the row the search box already took. */
@media (max-width:640px) { .site--portal .ctool__ward { flex:1 1 100%; min-width:0; } }

/* ---- announcement cards, on the portal pattern ----
   One card everywhere a notice is met (the landing band, the archive, the
   "more" list under a notice). The picture in a fixed 16:10 frame that
   zooms a touch under the pointer; the pinned tag on the picture; the title
   held to two lines and the text to three, so a row of cards is a row; and a
   foot with the date and a round arrow, pinned to the bottom of the card.
   The coloured strip along the top and the heavy plate were decoration
   that read as categories nobody had chosen, so they are gone. */
.site .ncard,
.site .ncard[data-tone],
.site .ncard.is-pinned {
  position:relative; display:flex; flex-direction:column; overflow:hidden;
  background:#fff; border:1px solid var(--line); border-radius:18px;
  box-shadow:0 4px 18px rgb(15 23 42 / .05);
  transition:transform .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.site .ncard[data-tone]::before { content:none; }
.site .ncard:hover,
.site .ncard[data-tone]:hover {
  transform:translateY(-4px); border-color:color-mix(in srgb, var(--brand-600) 30%, var(--line));
  box-shadow:0 18px 38px rgb(15 23 42 / .11);
}
.ncard__media { position:relative; aspect-ratio:16 / 10; overflow:hidden; background:var(--muted-soft); }
.site .ncard__img, .site .ncard__plate { width:100%; height:100%; display:block; }
.site .ncard__img { object-fit:cover; transition:transform .5s ease; }
.site .ncard:hover .ncard__img { transform:scale(1.05); }
.site .ncard__plate {
  display:grid; place-items:center;
  background:
    radial-gradient(circle at 78% 22%, color-mix(in srgb, var(--tone, var(--brand-600)) 20%, transparent) 0 26%, transparent 27%),
    linear-gradient(140deg, color-mix(in srgb, var(--tone, var(--brand-600)) 9%, #fff), color-mix(in srgb, var(--tone, var(--brand-600)) 22%, #fff));
  color:color-mix(in srgb, var(--tone, var(--brand-600)) 70%, #000);
}
.site .ncard__plate svg { opacity:.45; }
.site .ncard__media .ncard__pin {
  position:absolute; left:14px; bottom:14px; z-index:1;
  display:inline-flex; align-items:center; gap:5px;
  padding:5px 11px; border-radius:999px; font-size:12px; font-weight:700;
  background:var(--brand-600); color:#fff; box-shadow:0 4px 12px rgb(0 0 0 / .18);
}
.site .ncard__text { display:flex; flex-direction:column; flex:1 1 auto; padding:18px 20px 16px; }
.site .ncard__t {
  margin:0; font-size:17px; font-weight:750; line-height:1.4;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.site .ncard__t a::after { border-radius:18px; }
.site .ncard:hover .ncard__t a { text-decoration:none; color:var(--brand-800); }
.site .ncard__b {
  margin:8px 0 0; font-size:14px; line-height:1.6; color:var(--text-2);
  -webkit-line-clamp:3;
}
.ncard__foot {
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  margin-top:auto; padding-top:14px; border-top:1px solid var(--line);
}
/* The least room between the words and the foot; auto above does the rest. */
.site .ncard__t, .site .ncard__b { margin-bottom:16px; }
.site .ncard__t + .ncard__b { margin-top:-8px; }
.ncard__date { display:inline-flex; align-items:center; gap:6px; font-size:13px; color:var(--text-2); }
.ncard__date svg { color:var(--text-3); flex:0 0 auto; }
.site .ncard__go,
.site .ncard[data-tone] .ncard__go {
  display:inline-flex; align-items:center; gap:8px; margin:0; padding:0;
  font-size:13px; font-weight:700; color:var(--brand-700);
}
.ncard__arrow {
  width:30px; height:30px; border-radius:50%; display:grid; place-items:center;
  border:1px solid var(--line-strong); color:var(--brand-700);
  transition:background .16s ease, color .16s ease, border-color .16s ease;
}
.site .ncard:hover .ncard__arrow { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
@media (prefers-reduced-motion:reduce) {
  .site .ncard:hover, .site .ncard:hover .ncard__img { transform:none; }
}
/* On a phone "see all" is a pill under the heading, lined up with its words
   (the icon tile is 50px and the gap 14px), not a bar the width of the page. */
@media (max-width:640px) {
  .site--portal .phead__all { width:auto; margin-inline-start:64px; padding:6px 12px; }
}

/* ---- the site's search fields, one design ----
   The announcement search (landing band and archive), the contact search and
   their neighbours were three near-misses of each other: 44px here and 46px
   there, a pill button beside a square box, a focus ring in the section's
   hue — red around the contact search, which read as an error — and a second
   ring drawn inside the box by the site-wide input:focus rule. Now: one
   50px height and 14px corner for the box, its select and its buttons; the
   brand green on focus everywhere; the inner input never draws its own ring;
   and the browser's blue clear cross replaced by a quiet one of our own. */
.site { --sf-h:50px; --sf-r:14px; }
.site .nfilter__q,
.site .nfilter__q--band,
.site .ctool__search {
  height:var(--sf-h); border-radius:var(--sf-r); padding:0 14px; gap:10px;
  background:#fff; border:1.5px solid var(--line-strong);
  box-shadow:0 1px 2px rgb(15 23 42 / .04);
  transition:border-color .14s ease, box-shadow .14s ease;
}
.site .nfilter__q:hover, .site .ctool__search:hover { border-color:color-mix(in srgb, var(--brand-600) 45%, var(--line-strong)); }
.site .nfilter__q:focus-within,
.site .ctool__search:focus-within {
  border-color:var(--brand-600);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.site .nfilter__q:focus-within .ctool__ic,
.site .ctool__search:focus-within .ctool__ic { color:var(--brand-600); }
.site .nfilter__q input,
.site .ctool__search input,
.site .hsearch__q {
  height:100%; border:0; border-radius:0; background:transparent; padding:0;
  font:inherit; font-size:15px; color:var(--text);
}
.site .nfilter__q input:focus,
.site .ctool__search input:focus,
.site .hsearch__q:focus { outline:none; box-shadow:none; border:0; }
.site .nfilter__q input::placeholder,
.site .ctool__search input::placeholder,
.site .hsearch__q::placeholder { color:#8a97a8; opacity:1; }
.site input[type=search]::-webkit-search-cancel-button {
  -webkit-appearance:none; appearance:none; width:18px; height:18px; cursor:pointer;
  background:no-repeat center / 18px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='10' fill='%23e2e8f0'/%3E%3Cpath d='M9 9l6 6M15 9l-6 6' stroke='%23475569' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* The selects beside them: the same box, with a chevron of our own. */
.site .ctool__ward select,
.site .nfilter__year select {
  height:var(--sf-h); border-radius:var(--sf-r); border:1.5px solid var(--line-strong);
  padding:0 42px 0 14px; font:inherit; font-size:14.5px; font-weight:600; color:var(--text);
  background:#fff no-repeat right 14px center / 16px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  -webkit-appearance:none; appearance:none; cursor:pointer;
  box-shadow:0 1px 2px rgb(15 23 42 / .04);
  transition:border-color .14s ease, box-shadow .14s ease;
}
.site .ctool__ward select:hover, .site .nfilter__year select:hover { border-color:color-mix(in srgb, var(--brand-600) 45%, var(--line-strong)); }
.site .ctool__ward select:focus, .site .nfilter__year select:focus {
  outline:none; border-color:var(--brand-600);
  box-shadow:0 0 0 4px color-mix(in srgb, var(--brand-500) 18%, transparent);
}

/* The buttons beside them: the same height and corner. The announcement
   band's button is the form's submit, so it is drawn as the action it is. */
.site .nfilter__go,
.site .nfilter__clear,
.site .ctool__clear,
.site .site__more--search .site__more-btn {
  height:var(--sf-h); border-radius:var(--sf-r); padding:0 20px;
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  font-size:14px; font-weight:700; white-space:nowrap; flex:0 0 auto;
}
.site .site__more--search .site__more-btn {
  background:var(--brand-600); border:1px solid var(--brand-600); color:#fff;
  box-shadow:0 6px 16px color-mix(in srgb, var(--brand-600) 25%, transparent);
}
.site .site__more--search .site__more-btn:hover,
.site .site__sec[data-tone] .site__more--search .site__more-btn:hover {
  background:var(--brand-700); border-color:var(--brand-700); color:#fff;
}
.site .ctool__clear, .site .nfilter__clear {
  background:#fff; border:1.5px solid var(--line-strong); color:var(--text-2);
}
.site .ctool__clear:hover, .site .nfilter__clear:hover { border-color:var(--brand-600); color:var(--brand-700); background:var(--brand-50); }
.site .site__more--search, .site .nfilter, .site .ctool { gap:12px; align-items:center; }
/* On a phone every control in these rows takes the whole width, the archive's
   search button included, so the column has one edge on each side. */
@media (max-width:640px) {
  .site .nfilter__q, .site .nfilter__year, .site .nfilter__go, .site .nfilter__clear, .site .ctool__clear { flex:1 1 100%; }
}

/* ---- Select2 on the public site ----
   Every dropdown on these pages is Select2 now, as it is everywhere else in
   the application — the ward picker already was, drawn at the office's 36px
   in a grey that matched nothing around it, while the contact and archive
   lists opted out and were the browser's own. Same widget, drawn here as the
   search fields beside it are: 50px, 14px corners, the brand ring on focus. */
.site .select2-container--default .select2-selection--single {
  height:var(--sf-h); min-height:var(--sf-h); border-radius:var(--sf-r);
  border:1.5px solid var(--line-strong); background:#fff;
  box-shadow:0 1px 2px rgb(15 23 42 / .04);
  transition:border-color .14s ease, box-shadow .14s ease;
}
.site .select2-container--default .select2-selection--single:hover { border-color:color-mix(in srgb, var(--brand-600) 45%, var(--line-strong)); }
.site .select2-container--default .select2-selection--single .select2-selection__rendered {
  line-height:calc(var(--sf-h) - 3px); padding-left:15px; padding-right:44px;
  font-size:14.5px; font-weight:600; color:var(--text);
}
.site .select2-container--default .select2-selection--single .select2-selection__placeholder { color:var(--text); }
/* The vendor's border triangle, replaced by the same chevron the search
   fields' neighbours used; it turns over while the list is open. */
.site .select2-container--default .select2-selection--single .select2-selection__arrow {
  height:calc(var(--sf-h) - 3px); width:40px; right:4px;
  background:no-repeat center / 16px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  transition:transform .18s ease;
}
.site .select2-container--default .select2-selection--single .select2-selection__arrow b { display:none; }
.site .select2-container--open .select2-selection--single .select2-selection__arrow { transform:rotate(180deg); }
.site .select2-container--default .select2-selection--single .select2-selection__clear {
  height:calc(var(--sf-h) - 3px); margin-right:38px; padding:0 4px;
  font-size:0; width:26px; justify-content:center;
  background:no-repeat center / 18px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='10' fill='%23e2e8f0'/%3E%3Cpath d='M9 9l6 6M15 9l-6 6' stroke='%23475569' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
}
.site .select2-container--default .select2-selection--single .select2-selection__clear span { display:none; }
.site .select2-container--default.select2-container--focus .select2-selection--single,
.site .select2-container--default.select2-container--open .select2-selection--single {
  border-color:var(--brand-600); outline:0;
  box-shadow:0 0 0 4px color-mix(in srgb, var(--brand-500) 18%, transparent);
}

/* Its list floats on <body>, outside .site, so it is reached through the page
   that holds a .site. The panel is a card: rounded, lifted, a search box that
   looks like the ones on the page, and rows with room around them. */
body:has(.site) .select2-dropdown {
  border:1px solid var(--line); border-radius:14px; overflow:hidden;
  box-shadow:0 18px 44px rgb(15 23 42 / .16), 0 2px 6px rgb(15 23 42 / .06);
}
body:has(.site) .select2-container--open .select2-dropdown--below { margin-top:6px; }
body:has(.site) .select2-container--open .select2-dropdown--above { margin-top:-6px; }
body:has(.site) .select2-container--default .select2-search--dropdown { padding:10px 10px 6px; }
body:has(.site) .select2-container--default .select2-search--dropdown .select2-search__field {
  height:42px; border:1.5px solid var(--line-strong); border-radius:10px; padding:0 12px 0 36px;
  font-size:14.5px; outline:none;
  background:#fff no-repeat 11px center / 16px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E");
}
body:has(.site) .select2-container--default .select2-search--dropdown .select2-search__field:focus {
  border-color:var(--brand-600); box-shadow:0 0 0 4px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
body:has(.site) .select2-container--default .select2-results > .select2-results__options { padding:6px; }
body:has(.site) .select2-container--default .select2-results__option {
  padding:10px 12px; border-radius:9px; font-size:14.5px;
}
body:has(.site) .select2-container--default .select2-results__option + .select2-results__option { margin-top:2px; }
body:has(.site) .select2-container--default .select2-results__option--selected { background:var(--brand-50); color:var(--brand-800); }
body:has(.site) .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable,
body:has(.site) .select2-container--default .select2-results__option--highlighted.select2-results__option--selected {
  background:var(--brand-600); color:#fff;
}

/* The collection-times picker: its label a real label, not a whisper, and
   the "all wards" button beside it on the same 50px line. */
.site .wfilter__lbl { display:block; margin:0 0 6px; font-size:12.5px; font-weight:700; color:var(--text-2); }
.site .wfilter__row { display:flex; gap:10px; align-items:center; }
.site .wfilter__row .select2-container { flex:1 1 auto; }
.site .wfilter__row .btn {
  height:var(--sf-h); border-radius:var(--sf-r); padding:0 16px; white-space:nowrap;
  background:#fff; border:1.5px solid var(--line-strong); color:var(--text-2); font-weight:700;
}
.site--portal .phead .wfilter { min-width:260px; }

/* ---- keyboard focus, one way across the site ----
   The audit found four different answers: the browser's own 1px black ring
   on most links and cards (invisible on the green hero and footer), a 16%
   tint on .btn that other rules then painted over — so the hero's buttons
   and the map's showed nothing — green on the slider's arrows, white on its
   dots. Now: a 3px outline in the brand green with a 3px gap, following each
   element's corners; white where the ground is dark; and the text fields,
   which draw their own ring on their frame, left to do that. Mouse clicks do
   not show it — :focus-visible is the keyboard's. */
.site :where(a, button, summary, [role="tab"]):focus-visible,
.site .btn:focus-visible {
  outline:3px solid var(--brand-600); outline-offset:3px;
  box-shadow:none;
}
.site :where(.site__hero, .site__foot, .site__closer, .slider__cap, .site__slider) :where(a, button):focus-visible,
.site .site__hero .btn:focus-visible,
.site .site__closer .btn:focus-visible { outline-color:#fff; }
/* Inside the hero's white search bar the ground is white again. */
.site .hsearch__form :where(a, button):focus-visible { outline-color:var(--brand-800); outline-offset:2px; }
/* A card whose link is stretched over it with ::after: the outline goes on
   the card, not on the few words of its title. */
.site .ncard__t a:focus-visible { outline:none; }
.site .ncard:has(.ncard__t a:focus-visible) { outline:3px solid var(--brand-600); outline-offset:3px; }
/* The slider's picture link fills the frame, whose overflow would clip an
   outside outline; it goes inside instead. */
.site .slider__in:focus-visible { outline:3px solid #fff; outline-offset:-6px; }
/* Select2's control is a span with a tabindex; its ring is the frame above. */
.site .select2-selection:focus-visible { outline:none; }
/* Select2 squares the control's corners on the side its list opens, for a
   list that is attached; this one floats 6px off, so the corners stay. */
.site .select2-container--open.select2-container--below .select2-selection--single,
.site .select2-container--open.select2-container--above .select2-selection--single { border-radius:var(--sf-r); }

/* ---- links, one set of rules for the public site ----
   From the link audit (every link resolves; these are how they sit). */

/* Where a jump link lands. The header is sticky, and "track a report", the
   skip link and the steps scrolled their target 61px under it on a desktop
   and 79px on a phone — the box somebody jumped to type into was behind the
   bar — while the sections, at 74px, cleared it on a desktop and not on a
   phone, where the brand wraps to two lines. One margin for every anchor,
   sized to the bar at each width, with the preview bar added when it shows. */
.site [id] { scroll-margin-top:80px; }
@media (max-width:899px) { .site [id] { scroll-margin-top:96px; } }
.site--preview [id] { scroll-margin-top:120px; }
@media (max-width:899px) { .site--preview [id] { scroll-margin-top:136px; } }

/* One arrow. Some links ended in a typed "→" and some in the drawn arrow;
   they are all the drawn one now, sitting on the text's middle and nudging
   forward under the pointer. */
.lnk-arrow { display:inline-flex; vertical-align:middle; margin-inline-start:2px; transition:transform .16s ease; }
.lnk-arrow svg { display:block; }
a:hover .lnk-arrow, button:hover .lnk-arrow, a:hover > .otile__text .lnk-arrow { transform:translateX(3px); }
.site .site__foot-report, .site .otile__go { display:inline-flex; align-items:center; gap:6px; }
.site .phead__all svg, .site .pbanner__go svg { transition:transform .16s ease; }
.site .phead__all:hover svg, .site .pbanner__go:hover svg { transform:translateX(3px); }
@media (prefers-reduced-motion:reduce) { .lnk-arrow, .site .phead__all svg, .site .pbanner__go svg { transition:none; } }

/* A link inside a sentence is underlined, always — colour alone is not a
   thing everybody can see, and "Missed a collection? Tell us." was green
   text in a grey sentence. Links that are buttons, cards or rows carry a
   class and are not touched. */
.site :where(p, .site__aside, .site__note) a:not([class]) {
  color:var(--brand-700); font-weight:650;
  text-decoration:underline; text-decoration-thickness:1.5px; text-underline-offset:3px;
  text-decoration-color:color-mix(in srgb, var(--brand-600) 45%, transparent);
}
.site :where(p, .site__aside, .site__note) a:not([class]):hover { color:var(--brand-800); text-decoration-color:currentColor; }

/* Where you are, on the header's jump links and in the drawer. */
.site .site__nav a[aria-current="true"] { color:var(--brand-700); background:var(--brand-50); font-weight:700; }
.site .site__drawer-link[aria-current="true"] { color:var(--brand-700); background:var(--brand-50); font-weight:700; }

/* ---- the three panels: the film, quick links, the code ----
   One anatomy for all three — head (tile, title, a line on what it is for),
   body, and actions pinned to the foot — so the row lines up at the top and
   the bottom whatever each holds. Three across on a desktop; on a tablet
   two, with an odd one out taking the whole row and laying itself out
   sideways instead of sitting at half width beside nothing. */
.site--portal .ptri { align-items:stretch; }
@media (min-width:760px) and (max-width:1099px) {
  .site--portal .ptri { grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .site--portal .ptri > .ptile:last-child:nth-child(odd) { grid-column:1 / -1; }
}
@media (min-width:1100px) {
  .site--portal .ptri { grid-template-columns:repeat(auto-fit, minmax(0, 1fr)); }
}
.site--portal .ptile { display:flex; flex-direction:column; container-type:inline-size; padding:22px; }
.site--portal .ptile .ppanel__head { align-items:flex-start; }
.ppanel__hd { min-width:0; }
.site--portal .ptile .ppanel__t { font-size:17px; line-height:1.3; }
.ppanel__sub { margin:3px 0 0; font-size:13px; line-height:1.5; color:var(--text-2); }
.ptile__body { flex:1 1 auto; display:flex; flex-direction:column; min-height:0; }
.ptile__acts { display:grid; gap:10px; margin-top:18px; }
.ptile__acts--two { grid-template-columns:1fr 1fr; }
@container (max-width:330px) { .ptile__acts--two { grid-template-columns:1fr; } }
.site--portal .ptile__btn {
  display:inline-flex; align-items:center; justify-content:center; gap:8px; min-width:0;
  height:46px; padding:0 14px; border-radius:12px; font-size:14px; font-weight:700;
  background:#fff; color:var(--text); border:1.5px solid var(--line-strong); text-align:center;
  transition:background .14s ease, border-color .14s ease, color .14s ease;
}
.site--portal .ptile__btn span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.site--portal .ptile__btn svg { flex:0 0 auto; }
.site--portal .ptile__btn:hover { border-color:var(--brand-600); color:var(--brand-700); background:var(--brand-50); }
.site--portal .ptile__btn--go { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
.site--portal .ptile__btn--go:hover { background:var(--brand-700); border-color:var(--brand-700); color:#fff; }

/* The film: a dark stage the height of the row, the film centred on it at
   its own shape — a phone-shot Short stays 9:16 without making the whole
   row as tall as a phone. */
.ptile__body--film {
  align-items:center; justify-content:center;
  background:#0b1f1b; border-radius:14px; overflow:hidden; min-height:260px;
}
.site--portal .ptile__body--film .vplay { border-radius:0; width:100%; margin:0; }
.site--portal .ptile__body--film .vplay--tall {
  width:auto; height:100%; max-height:400px; min-height:260px; max-width:100%;
  aspect-ratio:9 / 16;
}
.ptile__body--film .vplay__file { width:100%; display:block; }

/* Quick links: rows a thumb can hit. */
.site--portal .pquick__list { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:4px; }
.site--portal .pquick__a {
  display:flex; align-items:center; gap:12px; min-height:46px; padding:6px 10px 6px 6px;
  border-radius:12px; color:var(--text); font-size:14.5px; font-weight:600;
  transition:background .14s ease, color .14s ease;
}
.pquick__ic {
  width:34px; height:34px; flex:0 0 34px; border-radius:10px; display:grid; place-items:center;
  background:var(--brand-50); color:var(--brand-700);
}
.pquick__l { flex:1 1 auto; min-width:0; line-height:1.35; }
.pquick__chev { display:flex; color:var(--text-3); transition:transform .14s ease, color .14s ease; }
.site--portal .pquick__a:hover { background:var(--brand-50); color:var(--brand-800); }
.site--portal .pquick__a:hover span { text-decoration:none; }
.site--portal .pquick__a:hover .pquick__ic { background:#fff; box-shadow:0 0 0 1px color-mix(in srgb, var(--brand-600) 22%, transparent); }
/* The form's own tile is green already; on hover it deepens rather than
   going white, which left a white flag on a white square. */
.site--portal .pquick__a--go:hover .pquick__ic { background:var(--brand-700); box-shadow:none; }
.site--portal .pquick__a--quiet:hover .pquick__ic { color:var(--brand-700); }
.site--portal .pquick__a:hover .pquick__chev { color:var(--brand-700); transform:translateX(3px); }
.pquick__a--go .pquick__ic { background:var(--brand-600); color:#fff; }
.site--portal .pquick__a--go { color:var(--brand-800); font-weight:700; }
.site--portal .pquick__a--quiet { color:var(--text-2); font-weight:500; }
.pquick__a--quiet .pquick__ic { background:var(--surface-2); color:var(--text-2); }

/* The code: the square on a soft plate, its address as a copyable line. */
.pqr__body { align-items:center; justify-content:center; gap:14px; }
.site--portal .pqr .pqr__code {
  width:196px; height:196px; padding:12px; background:#fff; margin:0;
  border:1px solid var(--line); border-radius:16px;
  box-shadow:0 0 0 8px var(--brand-50);
}
.pqr__url {
  display:flex; align-items:center; gap:8px; width:100%; max-width:340px; min-width:0;
  height:44px; padding:0 4px 0 12px; border-radius:12px;
  background:var(--surface-2); border:1px solid var(--line);
}
.pqr__url-ic { display:flex; color:var(--text-3); flex:0 0 auto; }
.pqr__url-v {
  flex:1 1 auto; min-width:0; font-size:13.5px; font-weight:600; color:var(--text);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-variant-numeric:tabular-nums;
}
.pqr__copy {
  flex:0 0 auto; height:34px; padding:0 12px; border-radius:9px; cursor:pointer;
  background:#fff; border:1px solid var(--line-strong); color:var(--brand-700);
  font:inherit; font-size:13px; font-weight:700;
}
.pqr__copy:hover { border-color:var(--brand-600); background:var(--brand-50); }
.pqr__copy.is-done { background:var(--brand-600); border-color:var(--brand-600); color:#fff; }
/* Given the whole row on a tablet: the square beside its words. */
@container (min-width:560px) {
  .pqr__body { flex-direction:row; justify-content:flex-start; gap:28px; }
  .pqr__url { max-width:none; flex:1 1 auto; }
  .pqr .ptile__acts--two { max-width:none; }
}

/* ---- the film, compact ----
   A 16:10 frame like the announcement pictures. For a Short, YouTube's still
   is 4:3 with the portrait frame in its middle 42% and black either side, so
   the frame is drawn twice: blurred and enlarged to fill (the black bars
   fall outside the crop), and sharp at 9:16 in the middle — sized so that
   exactly the film's own pixels show and none of the bars. */
.pfilm {
  position:relative; aspect-ratio:16 / 10; width:100%; overflow:hidden;
  border-radius:14px; background:#0b1f1b; isolation:isolate;
}
.pfilm__face { position:absolute; inset:0; }
.pfilm__blur {
  position:absolute; inset:-24px; z-index:-1;
  background:var(--still) center / 240% auto no-repeat;
  filter:blur(18px) brightness(.62) saturate(1.15);
}
.pfilm__still {
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  transform:scale(1.18);             /* a landscape film's letterbox bars */
  transition:transform .4s ease;
}
.pfilm--tall .pfilm__still {
  /* The middle of the 4:3 still at 9:16: a box that tall and that narrow,
     showing the image at the frame's height, centred. */
  left:50%; right:auto; width:auto; height:100%; aspect-ratio:9 / 16;
  transform:translateX(-50%); object-fit:cover; object-position:center;
  box-shadow:0 0 40px rgb(0 0 0 / .45);
}
.pfilm:not(.pfilm--tall):hover .pfilm__still { transform:scale(1.24); }
/* The frame is dark wherever the picture is not, so the controls on it are
   white; a soft shade at the foot keeps the hint readable over any still. */
.pfilm__face::after {
  content:''; position:absolute; inset:auto 0 0 0; height:46%; pointer-events:none;
  background:linear-gradient(0deg, rgb(0 0 0 / .55), transparent);
}
.pfilm__yt {
  position:absolute; left:12px; top:12px; z-index:1;
  display:inline-flex; align-items:center; gap:6px; padding:5px 10px 5px 8px;
  border-radius:999px; background:rgb(0 0 0 / .55); color:#fff;
  font-size:12px; font-weight:700; letter-spacing:.01em;
  backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
}
.pfilm__yt svg { color:#ff3b30; }
.pfilm__go {
  position:absolute; left:50%; top:50%; z-index:2; transform:translate(-50%, -50%);
  width:64px; height:64px; border-radius:50%; border:0; cursor:pointer;
  display:grid; place-items:center;
  background:rgb(255 255 255 / .94); box-shadow:0 10px 30px rgb(0 0 0 / .35);
  transition:transform .16s ease, background .16s ease;
}
/* The whole frame is the button's target — a small circle in the middle of a
   picture is a poor thing to ask a thumb to find. */
.pfilm__go::before { content:''; position:absolute; inset:-400px; }
.pfilm__tri {
  width:0; height:0; margin-left:5px;
  border-style:solid; border-width:11px 0 11px 18px;
  border-color:transparent transparent transparent var(--brand-700);
}
.pfilm:hover .pfilm__go { transform:translate(-50%, -50%) scale(1.08); background:#fff; }
.site .pfilm__go:focus-visible { outline:3px solid #fff; outline-offset:4px; }
.pfilm__hint {
  position:absolute; left:12px; bottom:10px; z-index:1;
  display:inline-flex; align-items:center; gap:5px;
  font-size:12px; font-weight:600; color:rgb(255 255 255 / .92);
}
.pfilm__out {
  position:absolute; left:50%; bottom:12px; transform:translateX(-50%); z-index:3;
  padding:6px 12px; border-radius:999px; background:#fff; color:var(--text); font-size:12.5px; font-weight:700;
}
.pfilm__frame { position:absolute; inset:0; width:100%; height:100%; border:0; display:block; }
.pfilm__file { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; background:#000; }
/* Full screen: the frame is the whole screen, and black around the film. */
.pfilm:fullscreen { aspect-ratio:auto; border-radius:0; background:#000; }
.pfilm:-webkit-full-screen { aspect-ratio:auto; border-radius:0; background:#000; }
@media (prefers-reduced-motion:reduce) {
  .pfilm__still, .pfilm__go { transition:none; }
  .pfilm:not(.pfilm--tall):hover .pfilm__still { transform:scale(1.18); }
}
/* The panel no longer stretches the film to the row: it sits at the top,
   under its heading, like the other panels' first thing. */
.site--portal .site__video .pfilm { flex:0 0 auto; }

/* ---- the code, for a phone camera ----
   What makes a code easy to scan: big modules, pure black on pure white, a
   white quiet zone round it (now in the SVG itself, four modules), and edges
   drawn square rather than smoothed into grey. So the square is larger, its
   plate adds no padding of its own and keeps no rounded clip over the code,
   and the paths are drawn crisp. */
.site--portal .pqr .pqr__code {
  display:block; width:min(248px, 100%); aspect-ratio:1; height:auto; padding:0; margin:0;
  background:#fff; border:1px solid var(--line); border-radius:14px; overflow:hidden;
  box-shadow:0 0 0 8px var(--brand-50); cursor:zoom-in;
  transition:box-shadow .16s ease, transform .16s ease;
}
.site--portal .pqr .pqr__code:hover { box-shadow:0 0 0 8px var(--brand-100); transform:scale(1.015); }
.pqr__code svg, .qrbig__code svg { display:block; width:100%; height:100%; shape-rendering:crispEdges; }
.pqr__code svg path, .qrbig__code svg path { shape-rendering:crispEdges; }
.pqr__tap {
  display:inline-flex; align-items:center; gap:5px; margin:2px 0 0;
  font-size:12.5px; font-weight:600; color:var(--text-2);
}
/* Only where the reader is on a phone: coarse pointer, narrow screen. */
.pqr__phone { display:none; }
@media (max-width:640px), (pointer:coarse) and (max-width:900px) {
  .pqr__phone {
    display:flex; align-items:flex-start; gap:8px; margin:0; width:100%;
    padding:10px 12px; border-radius:12px; font-size:13px; line-height:1.5;
    background:var(--info-soft); color:color-mix(in srgb, var(--info) 70%, #000);
  }
  .pqr__phone svg { flex:0 0 auto; margin-top:2px; }
  .pqr__tap { display:none; }
  /* On a phone the square is for showing to somebody else's phone. */
  .site--portal .pqr .pqr__code { width:min(280px, 100%); }
}
/* On a tablet the panel has the whole row: a bigger square beside its words. */
@container (min-width:560px) {
  .site--portal .pqr .pqr__code { width:260px; flex:0 0 260px; }
  .pqr__body { flex-wrap:wrap; align-content:center; }
  .pqr__tap { order:3; flex-basis:100%; }
}

/* The code full screen: white, the square as large as the screen allows,
   and nothing near it that a camera could mistake for part of it. */
.qrbig {
  width:100vw; height:100dvh; max-width:none; max-height:none; margin:0; padding:0;
  border:0; background:#fff; color:var(--text);
}
.qrbig::backdrop { background:#fff; }
.qrbig__in {
  height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:14px; padding:24px 16px calc(24px + env(safe-area-inset-bottom, 0px));
}
.qrbig__code { width:min(78vmin, 560px); aspect-ratio:1; }
.qrbig__t { margin:4px 0 0; font-size:clamp(17px, 2.4vw, 24px); font-weight:750; text-align:center; }
.qrbig__url { margin:0; font-size:clamp(14px, 1.8vw, 18px); color:var(--text-2); font-weight:600; overflow-wrap:anywhere; text-align:center; }
.qrbig__x {
  margin-top:8px; height:46px; padding:0 26px; border-radius:12px; cursor:pointer;
  background:var(--brand-600); border:0; color:#fff; font:inherit; font-size:15px; font-weight:700;
}
.qrbig__x:hover { background:var(--brand-700); }
.qrbig__x:focus-visible { outline:3px solid var(--brand-600); outline-offset:3px; }
/* Given the whole row (a tablet): the square on the left, and beside it the
   address and the hints stacked and centred on it. The flex row this replaces
   wrapped the address under the square and left the right half empty. */
@container (min-width:560px) {
  .pqr__body {
    display:grid; grid-template-columns:260px minmax(0, 1fr);
    grid-template-areas:"code ." "code url" "code tap" "code phone" "code .";
    grid-template-rows:1fr auto auto auto 1fr;
    column-gap:28px; row-gap:10px; align-items:center; justify-items:start;
  }
  .site--portal .pqr .pqr__code { grid-area:code; width:260px; }
  .pqr__url { grid-area:url; width:100%; max-width:none; }
  .pqr__tap { grid-area:tap; order:0; flex-basis:auto; }
  .pqr__phone { grid-area:phone; }
}
/* A row's actions sit on one centre line. The bin has no words, so it has no
   text baseline, and in a cell where the buttons beside it do it rode about
   5px high — hard to miss now that it is the red one. */
:is(.tbl, table) td > :is(.btn, .del-form, form) { vertical-align:middle; }

/* complaints: row actions in the register, and the details-change log */
.crow-acts { display:flex; gap:6px; align-items:center; flex-wrap:nowrap; }
.crow-acts .btn { display:inline-flex; align-items:center; gap:5px; }
.edit-log { list-style:none; margin:0; padding:0; display:grid; gap:12px; }
.edit-log__i { padding-bottom:12px; border-bottom:1px solid var(--line); }
.edit-log__i:last-child { padding-bottom:0; border-bottom:0; }
.edit-log__meta { font-size:12px; color:var(--text-3); margin-bottom:4px; }
.edit-log__changes { margin:0; padding-inline-start:18px; font-size:13px; line-height:1.55; color:var(--text-2); }

/* complaints: the status card on a complaint's page */
.cstat__steps { list-style:none; margin:4px 0 0; padding:0; display:grid; grid-template-columns:repeat(3, 1fr); }
.cstat__s { position:relative; text-align:center; padding-top:32px; }
.cstat__s::before, .cstat__s::after { content:''; position:absolute; top:11px; height:3px; background:var(--line); }
.cstat__s::before { left:0; right:50%; } .cstat__s::after { left:50%; right:0; }
.cstat__s:first-child::before, .cstat__s:last-child::after { display:none; }
.cstat__dot {
  position:absolute; top:0; left:50%; transform:translateX(-50%); z-index:1;
  width:25px; height:25px; border-radius:50%; display:grid; place-items:center;
  background:var(--surface); border:3px solid var(--line-strong); color:#fff;
}
.cstat__s.is-done .cstat__dot, .cstat__s.is-now .cstat__dot { background:var(--brand-600); border-color:var(--brand-600); }
.cstat__s.is-done::before, .cstat__s.is-done::after, .cstat__s.is-now::before { background:var(--brand-600); }
.cstat__s.is-now .cstat__dot { box-shadow:0 0 0 5px color-mix(in srgb, var(--brand-600) 20%, transparent); }
.cstat__s.is-now.is-rejected .cstat__dot { background:var(--bad); border-color:var(--bad); box-shadow:0 0 0 5px color-mix(in srgb, var(--bad) 20%, transparent); }
.cstat__l { font-size:13px; font-weight:650; color:var(--text-3); }
.cstat__s.is-done .cstat__l, .cstat__s.is-now .cstat__l { color:var(--text-1); }
.cstat__acts { display:flex; flex-wrap:wrap; gap:8px; margin-top:18px; padding-top:16px; border-top:1px solid var(--line); }
.cstat__acts .btn { display:inline-flex; align-items:center; gap:7px; }
.cstat__btn--bad { color:var(--bad); border-color:color-mix(in srgb, var(--bad) 40%, var(--line)); }
.cstat__btn--bad:hover { background:color-mix(in srgb, var(--bad) 8%, var(--surface)); color:var(--bad); }
.cstat__hint { margin:10px 0 0; font-size:12px; color:var(--text-3); }
@media (max-width:520px) { .cstat__acts .btn { flex:1 1 100%; justify-content:center; } }

/* complaints: the office's complaint form, in three groups on a fixed
   two-column grid */
.cform__sec + .cform__sec { margin-top:22px; padding-top:20px; border-top:1px solid var(--line); }
.cform__h { display:flex; align-items:center; gap:10px; margin:0 0 14px; font-size:15px; font-weight:750; }
.cform__n {
  width:26px; height:26px; border-radius:50%; display:grid; place-items:center; flex:0 0 auto;
  background:var(--brand-50); color:var(--brand-700); font-size:13px; font-weight:800;
}
.cform__grid, .cform__grid[class] { grid-template-columns:repeat(2, minmax(0, 1fr)); gap:14px 18px; }
@media (max-width:720px) { .cform__grid, .cform__grid[class] { grid-template-columns:minmax(0, 1fr); } }

/* complaints: a complaint's own page */
.cpage__back { display:inline-flex; align-items:center; gap:6px; margin-bottom:8px; font-size:13px; font-weight:600; color:var(--text-2); }
.cpage__back:hover { color:var(--brand-700); }
.cpage__sub { display:flex; flex-wrap:wrap; align-items:center; gap:6px 8px; }
.cpage__when { display:inline-flex; align-items:center; gap:5px; color:var(--text-3); font-size:12.5px; }
.cstat__d { display:block; font-size:11px; color:var(--text-3); margin-top:2px; }

/* the status change, on the page */
.sform { margin-top:18px; padding-top:18px; border-top:1px solid var(--line); }
.sform__to { border:0; margin:0; padding:0; min-width:0; }
.sform__label { padding:0; margin-bottom:8px; font-size:12.5px; font-weight:700; color:var(--text-2); }
.sform__opts { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:8px; }
.sopt { position:relative; cursor:pointer; }
.sopt input { position:absolute; opacity:0; pointer-events:none; }
.sopt__box {
  display:flex; flex-direction:column; align-items:center; gap:6px; height:100%; padding:12px 8px 10px;
  border:1.5px solid var(--line); border-radius:12px; background:var(--surface); text-align:center;
  transition:border-color .15s, background .15s, box-shadow .15s;
}
.sopt--open { --st:var(--info); } .sopt--in_progress { --st:var(--warn); }
.sopt--resolved { --st:var(--ok); } .sopt--rejected { --st:var(--bad); }
.sopt__ic {
  width:34px; height:34px; border-radius:50%; display:grid; place-items:center;
  background:color-mix(in srgb, var(--st) 12%, var(--surface)); color:var(--st);
}
.sopt__t { font-size:13px; font-weight:700; line-height:1.3; color:var(--text-1); }
.sopt__now {
  font-size:10.5px; font-weight:700; letter-spacing:.03em; padding:1px 8px; border-radius:999px;
  background:var(--surface-2); color:var(--text-3); border:1px solid var(--line);
}
.sopt:hover .sopt__box { border-color:color-mix(in srgb, var(--st) 45%, var(--line)); }
.sopt input:checked + .sopt__box {
  border-color:var(--st); background:color-mix(in srgb, var(--st) 7%, var(--surface));
  box-shadow:0 0 0 3px color-mix(in srgb, var(--st) 18%, transparent);
}
.sopt input:checked + .sopt__box .sopt__ic { background:var(--st); color:#fff; }
.sopt input:focus-visible + .sopt__box { outline:3px solid color-mix(in srgb, var(--st) 45%, transparent); outline-offset:2px; }
.sform__grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:14px 18px; margin-top:16px; }
.sform__grid textarea { min-height:44px; }
.sform__req { font-size:11px; font-weight:700; color:var(--bad); margin-inline-start:4px; }
.sform__photos { margin-top:14px; border:1px dashed var(--line-strong); border-radius:12px; background:var(--surface-2); }
.sform__photos > summary {
  display:flex; align-items:center; gap:8px; padding:11px 14px; cursor:pointer; list-style:none;
  font-size:13.5px; font-weight:650; color:var(--text-1);
}
.sform__photos > summary::-webkit-details-marker { display:none; }
.sform__photos > summary svg { color:var(--brand-600); }
.sform__opt { margin-inline-start:auto; font-size:11.5px; font-weight:600; color:var(--text-3); }
.sform__photos[open] > summary { border-bottom:1px dashed var(--line-strong); }
.sform__photos-in { padding:12px 14px 14px; display:grid; gap:10px; }
.sform__foot { display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-top:16px; }
.sform__save { display:inline-flex; align-items:center; gap:7px; min-width:160px; justify-content:center; }
.sform__save.is-busy { opacity:.7; cursor:progress; }
.sform__sum { font-size:13px; color:var(--text-3); font-weight:600; }
.sform__sum.is-move { color:var(--brand-700); }
@media (max-width:720px) {
  .sform__opts { grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .sform__grid { grid-template-columns:minmax(0, 1fr); }
  .sform__save { flex:1 1 100%; }
}

/* the history, as a timeline */
.ctl { list-style:none; margin:0; padding:0; position:relative; }
.ctl::before { content:''; position:absolute; left:7px; top:8px; bottom:8px; width:2px; background:var(--line); }
.ctl__i { position:relative; padding:0 0 18px 30px; }
.ctl__i:last-child { padding-bottom:0; }
.ctl__dot {
  position:absolute; left:0; top:4px; width:16px; height:16px; border-radius:50%;
  background:var(--surface); border:3px solid var(--tone, var(--brand-600));
}
.ctl__i--start .ctl__dot { border-color:var(--line-strong); }
.ctl__head { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.ctl__arrow { color:var(--text-3); }
.ctl__note {
  margin:8px 0 0; padding:9px 12px; border-radius:10px; background:var(--surface-2); border:1px solid var(--line);
  font-size:13.5px; line-height:1.55; white-space:pre-line; overflow-wrap:anywhere;
}
.ctl__i .ba-thumbs { margin-top:8px; }
.ctl__meta { display:flex; align-items:center; gap:5px; flex-wrap:wrap; margin-top:6px; font-size:12px; color:var(--text-3); }

/* the details, in groups */
.dgroup {
  display:flex; align-items:center; gap:6px; margin:16px 0 8px; font-size:11.5px; font-weight:750;
  letter-spacing:.04em; text-transform:uppercase; color:var(--text-3);
}
.dgroup:first-child { margin-top:0; }
.dgroup svg { color:var(--brand-600); }
.cdanger .card__icon { color:var(--bad); }

/* complaints: the photos card — a column per kind, each with its own add */
.pcols { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:16px; }
.pcols--one { grid-template-columns:minmax(0, 1fr); }
.pcol {
  display:flex; flex-direction:column; gap:12px; min-width:0; padding:14px;
  border:1px solid var(--line); border-radius:12px; background:var(--surface-2);
  border-top:3px solid var(--pc);
}
.pcol--before { --pc:var(--warn); } .pcol--after { --pc:var(--ok); } .pcol--evidence { --pc:var(--brand-600); }
.pcol__head { display:flex; align-items:center; gap:10px; }
.pcol__ic {
  flex:0 0 auto; width:30px; height:30px; border-radius:50%; display:grid; place-items:center;
  background:color-mix(in srgb, var(--pc) 14%, var(--surface)); color:var(--pc);
}
.pcol__t { min-width:0; flex:1 1 auto; }
.pcol__t h3 { margin:0; font-size:14px; font-weight:750; }
.pcol__t span { display:block; font-size:11.5px; color:var(--text-3); margin-top:1px; }
.pcol__n {
  min-width:26px; height:22px; padding:0 7px; border-radius:999px; display:grid; place-items:center;
  background:var(--surface); border:1px solid var(--line); font-size:12px; font-weight:700; color:var(--text-2);
}
.pcol__empty {
  margin:0; padding:16px 10px; border:1px dashed var(--line-strong); border-radius:10px;
  background:var(--surface); text-align:center; font-size:12.5px; color:var(--text-3);
}
.cphoto-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(104px, 1fr)); gap:10px; }
.cphoto { margin:0; border-radius:10px; overflow:hidden; background:var(--surface); border:1px solid var(--line); }
.cphoto a { display:block; aspect-ratio:1; }
.cphoto img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .2s; }
.cphoto a:hover img { transform:scale(1.04); }
.cphoto__cap { display:flex; align-items:center; gap:6px; padding:5px 6px 5px 8px; }
.cphoto__meta { flex:1 1 auto; min-width:0; font-size:11px; line-height:1.35; color:var(--text-3); }
.cphoto__by { display:inline-block; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom; color:var(--text-2); }
.cphoto__cap form { margin:0; }
.cphoto__del {
  width:28px; height:28px; border-radius:8px; display:grid; place-items:center; cursor:pointer;
  border:1px solid color-mix(in srgb, var(--bad) 35%, var(--line)); background:var(--surface); color:var(--bad);
}
.cphoto__del:hover { background:var(--bad); color:#fff; border-color:var(--bad); }
.pcol__add { margin-top:auto; padding-top:12px; border-top:1px dashed var(--line-strong); }
.pcol__add .btn-group { margin-bottom:0 !important; }
.pcol__add .btn-group .btn { background:var(--surface); }
.pcol__add .photo-grid:not(:empty) { margin-top:10px; }
.pcol__add .field__note { font-size:12px; }
.pcol__go { margin-top:10px; display:inline-flex; align-items:center; gap:6px; }
.pcol__go:disabled { display:none; }
@media (max-width:720px) { .pcols { grid-template-columns:minmax(0, 1fr); } }

/* ---- touch-sized controls ------------------------------------------------
   The compact controls are sized for a mouse: a 28px button, a 19px arrow, an
   8px slider dot. On a phone or tablet they sit under the 36–44px a thumb can
   hit reliably, and the wrong row's button is the one that gets pressed. Here,
   and only on a small or touch screen, every one of them gets a real target;
   the desktop layout is untouched. */
@media (max-width:720px), (pointer:coarse) {
  .btn { min-height:40px; }
  .btn--sm { min-height:36px; padding:0 12px; font-size:12.5px; }
  .perm-mini { padding:9px 11px; font-size:11.5px; }
  .stops__mv { width:40px; height:32px; font-size:11px; }
  .cphoto__del { width:34px; height:34px; }
  .fr-more { padding:9px 0; }
  /* The dot stays a dot; the area that answers the tap is larger. */
  .slider__dot { position:relative; }
  .slider__dot::after { content:''; position:absolute; inset:-12px -8px; }
  input[type=checkbox], input[type=radio] { width:20px; height:20px; }
  /* A checkbox's label is its target: the whole row answers the tap. */
  .check, .switch { min-height:36px; }
  .check { padding:4px 0; }
  .notif__actions .btn { min-height:36px; padding:0 10px; }
  .fleet__pager-nav select { height:36px; }
}
