:root {
  --bg: #0f1419;
  --panel: #161b22;
  --panel2: #1c222b;
  --border: #2a2f37;
  --text: #d5dae0;
  --muted: #7d8590;
  --police: #4aa3ff;
  --fire: #ff5a5a;
  --ambulance: #f2c744;
  --prio-high: #ff5a5a;
  --prio-med: #f59e0b;
  --prio-low: #29c467;
  /* No priority at all. Off the red-amber-green scale, and paler than --police, which the
       TEST outline uses. See docs/decisions/frontend.md#the-palette. */
  --prio-info: #8ecdf5;
  --mixed: #c084fc;
  /* The second capcode list's rail — the one hue that was still free.
       See docs/decisions/frontend.md#the-palette. */
  --related: #2bb3a3;
  /* The address register's own colour, the gap between the ambulance's amber and the
       brandweer's red. See docs/decisions/frontend.md#the-palette. */
  --bag: #dc8a4c;
  /* The building's surfaces: opaque and mixed per theme rather than computed at draw time,
       so there is no colour function to lean on. The shading multiplies them. */
  --face-wall: #6a4e3c;
  --face-roof: #9a6a45;
  /* The creases. Darker than the face in both themes — the key light carries #6a4e3c to a
       pale tan, so a light line lands on the bright faces invisibly.
       See docs/decisions/frontend.md#the-palette. */
  --face-edge: #4a3526;
  --sel: #253044;
  /* The switcher's selected item must read as raised — lighter than its track — in both
       themes, which --panel/--panel2 cannot do: they swap when the theme flips. */
  --seg-track: #12171d;
  --seg-active: #242c37;
  --seg-shadow: rgba(0, 0, 0, .45);
  /* Wide windows only: on a light ground the same track is a grey slab across the top of the
       window, so the light palettes drop it. See docs/decisions/frontend.md#the-palette. */
  --seg-track-wide: var(--seg-track);
  --seg-active-wide: var(--seg-active);
  --seg-edge-wide: transparent;

  /* A system stack; nothing is downloaded. The emoji families are for emoji arriving *inside
       a message* — the feed's own glyphs are drawn from icons.js.
       See docs/decisions/frontend.md#the-palette. */
  --brand: #fff;
  --rowline: #1a1f27;
  --rowhover: #12171e;
  --staroff: #3a4048;

  /* What the page keeps clear of the screen's edges. The panels inside the detail pane carry
       their own padding. See docs/decisions/frontend.md#the-shell. */
  --gutter: 6px;

  --font-ui:
    system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", "Liberation Sans", Arial,
    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
}

/* Light theme. Dark stays the default; this takes over when the platform asks for light
   unless the reader chose dark. The accents are not the dark ones reused — amber on white
   measures 1.43:1. See docs/decisions/frontend.md#the-palette. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) { color-scheme: light; }
}
:root[data-theme="light"] { color-scheme: light; }

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #ffffff;
    --panel: #f3f5f8;
    --panel2: #e7ebf1;
    --border: #ccd3dc;
    --text: #171c23;
    --muted: #5a6673;
    --police: #0b5fc4;
    --fire: #c62828;
    --ambulance: #8a6100;
    --prio-high: #c62828;
    --prio-med: #a15c00;
    --prio-low: #1b7f42;
    --prio-info: #2f8fbf;
    --mixed: #7127c8;
    --related: #10756c;
    --bag: #8a4a14;
    --face-wall: #cbb49f;
    --face-roof: #b08a63;
    --face-edge: #6f5641;
    --sel: #d8e6fb;
    --brand: #111820;
    --rowline: #e3e8ee;
    --rowhover: #eef2f7;
    --staroff: #b3bcc6;
    --seg-track: #e4e9f0;
    --seg-active: #ffffff;
    --seg-shadow: rgba(23, 28, 35, .16);
    --seg-track-wide: transparent;
    --seg-active-wide: #ffffff;
    --seg-edge-wide: #d5dce5;
  }
}

:root[data-theme="light"] {
  --bg: #ffffff;
  --panel: #f3f5f8;
  --panel2: #e7ebf1;
  --border: #ccd3dc;
  --text: #171c23;
  --muted: #5a6673;
  --police: #0b5fc4;
  --fire: #c62828;
  --ambulance: #8a6100;
  --prio-high: #c62828;
  --prio-med: #a15c00;
  --prio-low: #1b7f42;
  --prio-info: #2f8fbf;
  --mixed: #7127c8;
  --related: #10756c;
  --bag: #8a4a14;
  --face-wall: #cbb49f;
  --face-roof: #b08a63;
  --face-edge: #6f5641;
  --sel: #d8e6fb;
  --brand: #111820;
  --rowline: #e3e8ee;
  --rowhover: #eef2f7;
  --staroff: #b3bcc6;
  --seg-track: #e4e9f0;
  --seg-active: #ffffff;
  --seg-shadow: rgba(23, 28, 35, .16);
  --seg-track-wide: transparent;
  --seg-active-wide: #ffffff;
  --seg-edge-wide: #d5dce5;
}

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  background: var(--bg);
  color: var(--text);
  font: 13px/1.45 var(--font-ui);
  /* Digits are fixed-width within the proportional face, so times and capcodes line up
     in their columns without switching to a monospace font for them. */
  font-variant-numeric: tabular-nums;
}
/* 100dvh tracks Safari's collapsing toolbar; the vh line is the fallback. */
#app { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
/* vh, not dvh: iOS 26.0 computes dvh as if Safari's toolbar were still on screen, which on
   a pinned app leaves a toolbar's height of bare background below the view buttons.
   See docs/decisions/frontend.md#the-palette. */
@media (display-mode: standalone) {
  #app { height: 100vh; }
}

/* Status bar */
/* Sticky, which does nothing on the feed — a fixed-height flex column whose list scrolls
   inside itself — and everything on the account page, an ordinary scrolling document.
   Opaque background, so the form does not read through it. */
/* Present for a screen reader and for a crawler, absent for everyone else. The clip-path
   pair rather than display:none, which would take it out of the accessibility tree too. */
.sronly {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

.statusbar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 8px var(--gutter); background: var(--panel); border-bottom: 1px solid var(--border);
  padding-top: max(8px, env(safe-area-inset-top));
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}
.brand {
  display: inline-flex; align-items: center; gap: 9px;
  font-weight: 700; letter-spacing: .5px; color: var(--brand);
  text-decoration: none; white-space: nowrap;
}
.brand .logo { width: 32px; height: 32px; flex: none; display: block; }
/* ᛒᛁᚱᚷᛁᚱ in Elder Futhark, drawn as paths — no phone ships a runic font, so text
   would render as empty boxes. currentColor lets it mute against the name. */
/* Generated by tools/make-logo.py; the stroke colour is baked into the file. */
.brand .runes { height: 17px; width: auto; flex: none; display: block; }
/* No underline to give on hover — it is a drawing, not text. */
@media (hover: hover) { .brand:hover .runes { opacity: .72; } }
@media (hover: hover) { .brand:hover { text-decoration: underline; } }
.stat { color: var(--muted); white-space: nowrap; }
.stat.ok { color: var(--prio-low); }
.stat.warn { color: var(--prio-med); }
/* The live indicator is the control that reveals the receiver's numbers. */
.conn {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--muted); font: inherit; padding: 3px 7px; cursor: pointer;
  white-space: nowrap;
}
.conn.ok { color: var(--prio-low); }
.conn.warn { color: var(--prio-med); }
.conn .chev { color: var(--muted); font-size: .85em; }
@media (hover: hover) { .conn:hover { border-color: var(--border); } }
.conn:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }

.themebtn {
  margin-left: auto;   /* pushes itself and the live indicator to the right together */
  background: none; border: 1px solid transparent; border-radius: 6px;
  /* `font: inherit` inherits the font and not the colour: a <button> takes the UA's own
       buttontext, which is black on the dark theme. */
  color: var(--text);
  padding: 3px 6px; font: inherit; line-height: 1; cursor: pointer;
}
@media (hover: hover) { .themebtn:hover { border-color: var(--border); } }
.themebtn:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
@media (pointer: coarse) { .themebtn { min-width: 34px; min-height: 34px; } }

/* The wordmark is drawn in white strokes, which vanish on a pale ground. Inverting a
   monochrome mark is exact, and keeps one generated file rather than two. */
:root[data-theme="light"] .brand .runes { filter: invert(1); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .brand .runes { filter: invert(1); }
}

/* Full width so it wraps onto its own line inside the flex header. */
.statusdetail {
  flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 14px;
  padding-top: 8px; margin-top: 2px; border-top: 1px solid var(--border);
}
/* Each reading is a button: touch screens have no hover, so a title alone would leave a
   phone with no explanation at all. */
.statusdetail .stat {
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--muted); font: inherit; padding: 2px 6px; cursor: pointer;
  white-space: nowrap; text-align: left;
}
.statusdetail .stat.ok { color: var(--prio-low); }
.statusdetail .stat.warn { color: var(--prio-med); }
.statusdetail .stat.picked { border-color: var(--border); background: var(--panel2); color: var(--text); }
@media (hover: hover) { .statusdetail .stat:hover { border-color: var(--border); } }
.statusdetail .stat:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }

.stathint {
  flex-basis: 100%; margin: 2px 0 0; padding: 6px 8px;
  color: var(--text); background: var(--panel2);
  border: 1px solid var(--border); border-radius: 6px;
  font-size: .9em; line-height: 1.45;
}

@media (pointer: coarse) {
  .statusdetail .stat { min-height: 34px; padding: 6px 8px; }
}

/* The settings panel. Inside the header with flex-basis:100%, the same trick .statusdetail
   uses, so it grows the header rather than floating over the feed — which is also what
   settles dismissal: no outside-click handler, so clicking a message cannot throw away the
   panel you were reading. See docs/decisions/frontend.md#the-shell. */
.settingspanel {
  flex-basis: 100%; margin-top: 6px; padding-top: 8px;
  border-top: 1px solid var(--border);
}
.pgrid {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
/* One group per row on a phone: auto-fit alone fitted two 180px columns on a 430px handset,
   which reads as a form rather than a short list of settings. */
@media (max-width: 620px) {
  .pgrid { grid-template-columns: 1fr; }
}
.pgroup {
  border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 9px; background: var(--panel2);
}
.ptitle {
  display: block; margin-bottom: 6px; font-size: .78em; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted);
}
/* One row of the panel: a glyph, what it is, and what it currently says. A link and a
   button have to be indistinguishable here — half of these navigate and half toggle. */
.sitem {
  display: flex; align-items: center; gap: 9px; width: 100%;
  font: inherit; font-size: .95em; text-align: left; cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: 7px;
  color: var(--text); text-decoration: none; padding: 6px 7px;
}
@media (hover: hover) { .sitem:hover { border-color: var(--border); background: var(--panel); } }
.sitem:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
.sitem .sglyph { width: 20px; text-align: center; flex: none; }
.sitem .sstate { margin-left: auto; color: var(--muted); font-size: .85em; }
@media (pointer: coarse) { .sitem { min-height: 38px; } }
.settingsfoot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
/* The gear says whether it is open, because the panel it opens is below the fold on a
   phone — the chevron is the only part of it you can see from the top of the page. */
.gearbtn.on { border-color: var(--border); background: var(--panel2); }
.gearbtn .chev { font-size: .7em; margin-left: 2px; color: var(--muted); }
.alertflash {
  color: #201500; background: var(--ambulance); font-weight: 700;
  padding: 1px 7px; border-radius: 4px; animation: alertpulse 1.6s ease-out;
}
@keyframes alertpulse { from { opacity: 1; } to { opacity: .75; } }
@media (prefers-reduced-motion: reduce) { .alertflash { animation: none; } }

/* Toolbar */
.toolbar {
  display: flex; align-items: center; gap: 8px;
  padding: 8px var(--gutter); background: var(--panel2); border-bottom: 1px solid var(--border);
}
/* The input and its clear button travel together, so the search keeps its own row
   on a narrow screen instead of the button trailing onto the button row. */
.filterbox { display: flex; align-items: center; gap: 8px; flex: 1 1 240px; max-width: 460px; }
.filter {
  flex: 1; min-width: 0;
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); padding: 6px 10px; font: inherit;
}
.filter:focus { outline: none; border-color: var(--police); }
.btn {
  background: var(--panel); color: var(--text); border: 1px solid var(--border);
  border-radius: 6px; padding: 6px 10px; cursor: pointer; font: inherit;
  /* A filter button's label is the selection, and a hyphenated roepnummer broke across two
       lines and made the button twice as tall as its neighbours. The row wraps; labels do not. */
  white-space: nowrap;
}
@media (hover: hover) { .btn:hover { border-color: var(--muted); } }
.btn.active { border-color: var(--prio-med); color: var(--prio-med); }
.btn.newpill { border-color: var(--police); color: var(--police); }
/* Toolbar buttons collapse to their icon on a narrow screen; the label markup is
   always present so this stays pure CSS and the button keeps its accessible name. */
.btnicon { font-style: normal; }
/* Drawn rather than an emoji: there is no funnel in the emoji set and the near ones all
   mean something else (a magnifier is the search beside it). */
.funnel { vertical-align: -1px; }
.btn.filterbtn { display: inline-flex; align-items: center; gap: 2px; }
.btn.filterbtn .chev { color: var(--muted); margin-left: 4px; }
.btn.filterbtn.active .chev, .btn.filterbtn.haspreset .chev { color: inherit; }
/* Amber is "something is narrowing this"; blue is "you chose a bewaard filter, and this
   is its name". Two states of one button, because they are two answers to one question. */
.btn.filterbtn.haspreset { border-color: var(--police); color: var(--police); }
.btn.filterbtn.haspreset .btnbadge { color: var(--police); }
/* Groeperen is a way of looking, not a filter, so it takes neither the amber of an active
   filter nor the blue of a preset: on it reads as a pressed switch, which is what it is. */
.btn.groupbtn { display: inline-flex; align-items: center; }
.btn.groupbtn.active { background: var(--sel); color: var(--text); border-color: var(--police); }

/* A name can be typed up to 24 characters and the toolbar is one line. */
.btn.filterbtn .btnlabel { max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btnlabel { margin-left: 5px; }
.btnbadge { display: none; }

/* Discipline toggles */
.kindbtns { display: flex; gap: 6px; }
.kindbtn { white-space: nowrap; }
.kindbtn.active.k-fire { border-color: var(--fire); color: var(--fire); }
.kindbtn.active.k-ambulance { border-color: var(--ambulance); color: var(--ambulance); }
.kindbtn.active.k-police { border-color: var(--police); color: var(--police); }

/* One panel instead of five popup menus. .dropitem survives because a checkbox row reads
   the same wherever it is put. See docs/decisions/frontend.md#the-filter-panel. */
.dropitem {
  display: flex; align-items: center; gap: 8px; padding: 4px 6px;
  border-radius: 6px; cursor: pointer; font-size: .9em;
}
@media (hover: hover) { .dropitem:hover { background: var(--panel2); } }
.dropname { flex: 1; min-width: 0; }
.dropcount { color: var(--muted); font-size: .85em; }
.dropempty { color: var(--muted); padding: 8px 6px; font-size: .9em; }

/* ---- The filter panel ----------------------------------------------------------- */
/* Pushes the list down instead of floating over it. A popup would cover the thing you
   are filtering, and this is where you sit for a moment and set several things at once. */
.filterpanel {
  background: var(--panel); border-bottom: 1px solid var(--border); padding: 14px var(--gutter);
}
/* auto-fit rather than a fixed count: three columns on a desktop, one on a phone, without
   a breakpoint deciding it. */
.fgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px 22px; }
.fgroup { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
/* Periode is seven options that read as one line of choices, so it gets the full width
   rather than wrapping three deep in a narrow column. */
.fgroup.wide { grid-column: 1 / -1; }
.fhead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ftitle { font-weight: 600; font-size: .92em; }
.fclear {
  font: inherit; font-size: .8em; cursor: pointer; background: none; border: 0;
  color: var(--muted); text-decoration: underline; padding: 2px;
}
.fclear:disabled { opacity: .4; text-decoration: none; cursor: default; }
.fchips { display: flex; flex-wrap: wrap; gap: 6px; }
.fopt {
  font: inherit; font-size: .88em; cursor: pointer; white-space: nowrap;
  padding: 5px 10px; border-radius: 999px;
  background: var(--panel2); color: var(--text); border: 1px solid var(--border);
}
@media (hover: hover) { .fopt:hover { border-color: var(--muted); } }
.fopt.on { border-color: var(--prio-med); color: var(--prio-med); }
/* The discipline colours are the same ones the rows carry, so a picked dienst looks like
   the messages it will leave behind. */
.fopt.k-fire.on { border-color: var(--fire); color: var(--fire); }
.fopt.k-ambulance.on { border-color: var(--ambulance); color: var(--ambulance); }
.fopt.k-police.on { border-color: var(--police); color: var(--police); }
/* Only the webhook editor offers this one: a page whose capcodes the reference has never
   heard of. It has no colour of its own anywhere else, so it keeps the ordinary one. */
.fopt.k-other.on { border-color: var(--prio-med); color: var(--prio-med); }
.fcount { color: var(--muted); font-size: .85em; margin-left: 5px; }
/* A chip holding a badge is only a frame around it, and off drains the badge rather than
   changing its shape: the row keeps its rhythm and only the colour says what is selected. */
.fopt:has(.prio), .fopt:has(.dia) { padding: 4px 7px; }
.fopt:not(.on) .prio, .fopt:not(.on) .dia { filter: grayscale(1); opacity: .5; }
.fopt .dia { margin-right: 0; }
/* A roepnummer chip carries two actions — filter on it, or forget it — so it is a pair of
   buttons wearing one chip rather than one button with a hidden second meaning. */
.fopt.vehopt { display: inline-flex; align-items: stretch; gap: 0; padding: 0; overflow: hidden; }
.vehpick, .vehforget {
  font: inherit; font-size: inherit; cursor: pointer; background: none; border: 0;
  color: inherit; padding: 5px 10px;
}
.vehforget { color: var(--muted); padding-left: 6px; padding-right: 8px; }
@media (hover: hover) { .vehforget:hover { color: var(--fire); background: var(--panel); } }
/* Twenty-five regions do not belong in a wrapping chip cloud; they stay a list that
   scrolls inside its own box, the way the menu showed them. */
.flist {
  display: flex; flex-direction: column; gap: 2px; max-height: 190px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 8px; padding: 4px;
}
.fradius { display: flex; align-items: center; gap: 10px; font-size: .9em; }
.fradius input[type="range"] { flex: 1; min-width: 0; }
.fempty { color: var(--muted); font-size: .88em; }
/* Naming a roepnummer the shortlist does not have yet. Sits with the chips it adds to, so it
   reads as the end of that row. See docs/decisions/frontend.md#roepnummers. */
.vehadd { display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; }
.vehadd input {
  width: 7em; min-width: 0; font: inherit; font-size: .88em;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px;
}
.vehadd input:focus { outline: none; border-color: var(--police); }

/* A chip carries the number and what the reference says it is: a bare number in a filter
   tells its owner nothing a month later. */
.fopt.capopt { display: inline-flex; align-items: baseline; gap: 6px; padding-right: 2px; }
.lookupwhat { color: var(--muted); font-size: .85em; }
.lookup { position: relative; margin-top: 6px; }
.lookuphits {
  display: flex; flex-direction: column; gap: 2px;
  margin-top: 4px; max-height: 220px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 8px; padding: 4px; background: var(--panel);
}
.lookuphit {
  display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left;
  font: inherit; font-size: .88em; cursor: pointer;
  padding: 5px 8px; border-radius: 6px; border: 1px solid transparent;
  background: none; color: var(--text);
}
/* The number is what is being picked, so it keeps its line whatever the column does; the
   description beside it is the part that may be cut. */
.lookuphit b { white-space: nowrap; }
.lookuphit .lookupwhat { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (hover: hover) { .lookuphit:hover { background: var(--panel2); border-color: var(--border); } }
.lookuphit:focus-visible { outline: 2px solid var(--police); outline-offset: -2px; }
/* The same colour the feed gives that discipline, so a hit is recognisable before it is read. */
.lookuphit.svc-fire b { color: var(--fire); }
.lookuphit.svc-ambulance b { color: var(--ambulance); }
.lookuphit.svc-police b { color: var(--police); }
.fpanelfoot {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);
}

/* Inside the panel rather than a strip above it: the count on the button already says that
   something is on, and this says which once you have asked. */
/* ---- Bewaarde filters ------------------------------------------------------------ */
/* A row of names above the groups. Blue rather than the chips' amber: a preset is a whole
   set with a name, not one of the things in it. */
.presets {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px dashed var(--border);
}
.menuhost { position: relative; display: inline-flex; }
.fpreset {
  font: inherit; font-size: .88em; white-space: nowrap;
  padding: 5px 3px 5px 12px; border-radius: 999px;
  background: var(--panel2); color: var(--text); border: 1px solid var(--border);
  display: inline-flex; align-items: center; gap: 3px;
}
.fpreset.on { border-color: var(--police); color: var(--police); background: var(--sel); }
.fpick { font: inherit; background: none; border: 0; color: inherit; cursor: pointer; padding: 0; }
.fmore {
  border: 0; background: none; color: inherit; opacity: .55; cursor: pointer;
  padding: 0 6px; display: inline-flex; align-items: center; line-height: 1;
}
@media (hover: hover) { .fmore:hover { opacity: 1; } }
.fmore[aria-expanded="true"] { opacity: 1; }
.fpreset.add { padding: 5px 12px; border-style: dashed; color: var(--muted); cursor: pointer; }
@media (hover: hover) { .fpreset.add:hover:not(:disabled) { color: var(--text); border-color: var(--muted); } }
.fpreset.add:disabled { opacity: .45; cursor: default; }
/* The one mark that says a preset no longer matches what is on screen. Amber, because it
   is the loose filters that have moved away from it. */
.fdot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--prio-med);
  display: inline-block; flex: none; margin-left: 2px;
}
.fmenu {
  position: absolute; z-index: 30; top: calc(100% + 6px); left: 0; min-width: 236px;
  background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 5px;
  box-shadow: 0 14px 32px -14px rgba(0, 0, 0, .7); text-align: left;
}
.fmitem {
  display: flex; width: 100%; align-items: center; gap: 8px; font: inherit; font-size: .88em;
  background: none; border: 0; color: var(--text); padding: 7px 9px; border-radius: 6px;
  cursor: pointer; text-align: left;
}
@media (hover: hover) { .fmitem:hover { background: var(--panel2); } }
.fmitem.danger { color: var(--fire); }
.fmnote { font-size: .78em; color: var(--muted); padding: 4px 9px 4px; margin: 0; line-height: 1.45; }
.fmrow { display: flex; gap: 6px; padding: 6px 4px 2px; }
.fname {
  font: inherit; font-size: .88em; width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px;
}
.fname.wide { width: 190px; }
.fname:focus { outline: none; border-color: var(--police); }
.fdrift {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: -4px 0 12px; font-size: .85em; color: var(--muted);
}
.fdrift b { color: var(--prio-med); font-weight: 600; }
.flink {
  font: inherit; font-size: 1em; background: none; border: 0; padding: 2px;
  color: var(--police); text-decoration: underline; cursor: pointer;
}

.activefilters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-bottom: 16px; padding: 8px 10px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: 8px;
}
.factive { color: var(--muted); font-size: .8em; margin-right: 2px; }
.fchip {
  font: inherit; font-size: .85em; cursor: pointer; white-space: nowrap;
  padding: 3px 9px; border-radius: 999px;
  background: var(--panel); color: var(--prio-med); border: 1px solid var(--prio-med);
}
@media (hover: hover) { .fchip:hover { border-color: var(--fire); color: var(--fire); } }

/* "starred only" and its alert are one feature, so they sit together as one group. */
.starbtns { display: flex; gap: 6px; }
.starbtns .btn { white-space: nowrap; }
.btn.starfilter .btnicon, .btn.alertbtn.active .btnicon { color: var(--ambulance); }
/* The block a regio page carries after the app. Server-rendered and outside #app, so it is
   in the HTML before any script runs. See docs/decisions/pages.md#places-and-regions. */
.regiofoot {
  border-top: 1px solid var(--border); background: var(--panel);
  padding: 22px var(--gutter) 34px; color: var(--muted); font-size: .92em;
}
.regiofoot h2 { font-size: 1.05em; color: var(--text); margin: 0 0 8px; }
.regiofoot p { margin: 0 0 8px; max-width: 70ch; line-height: 1.55; }
.regiofoot a { color: var(--police); }

.spacer { flex: 1; }
.count { color: var(--muted); }

/* Layout */
.layout { flex: 1; display: flex; min-height: 0; }
/* min-height:0 as well as min-width:0 — a flex item's default min-height:auto refuses to
   shrink below its content and pushes the detail pane off the bottom of the screen. */
.feed { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.detail {
  flex: 0 0 auto; background: var(--panel);   /* width is set inline and dragged by .splitter */
  /* No padding at the top: .dhead carries it, so that nothing can scroll through the gap
     above a sticky header. */
  border-left: 1px solid var(--border); padding: 0 var(--gutter) 14px; overflow-y: auto;
}

/* Drag handle between the feed and the detail pane */
.splitter {
  flex: 0 0 5px; position: relative; cursor: col-resize;
  background: var(--border); transition: background .12s;
}
.splitter::after {          /* a forgiving hit area without a fat visible bar */
  content: ''; position: absolute; top: 0; bottom: 0; left: -4px; right: -4px;
}
.splitter:hover, body.resizing .splitter { background: var(--police); }
body.resizing { cursor: col-resize; user-select: none; }
body.resizing .leaflet-container { pointer-events: none; }   /* don't let the map eat the drag */

/* Feed rows (grid keeps columns aligned) */
.feedhead, .row {
  display: grid;
  grid-template-columns: 2.2em 7.3em 3.6em 1fr auto;
  grid-template-areas: "g t p m v";
  gap: 8px; align-items: baseline; padding: 3px var(--gutter);
}
.feedhead { color: var(--muted); border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); }
/* Setting one axis to auto computes the other to auto too, so this scrolled sideways the
   moment a row was wider than the phone. The list is only ever read vertically. */
.rows { flex: 1; overflow-y: auto; overflow-x: hidden; }
.row { cursor: pointer; border-bottom: 1px solid var(--rowline); }
@media (hover: hover) { .row:hover { background: var(--rowhover); } }
.row.selected { background: var(--sel); }
/* A starred page is marked by the edge alone: setting and clearing a star lives in the
   detail pane, where there is room to see which capcode it applies to. */
.row.starred { box-shadow: inset 3px 0 0 var(--ambulance); }

/* A call-out reads as one row, and the count is the whole signal: 18 says an incident took
   eighteen pages, which on a list is the thing worth seeing first. */
.grouprow .c-msg { font-weight: 500; }
/* The row shows when the call-out began; the list is ordered by its most recent page, and
   the span reconciles the two. Inline, not a second line — a grouped row standing taller
   than its neighbours breaks a list read at arm's length.
   See docs/decisions/frontend.md#grouped-rows. */
.gspan { margin-left: 7px; color: var(--muted); font-size: .85em; white-space: nowrap; }
.gkinds { display: inline-flex; gap: 3px; margin-left: 7px; vertical-align: middle; }
.gdot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.gdot.d-fire { background: var(--fire); }
.gdot.d-police { background: var(--police); }
.gdot.d-ambulance { background: var(--ambulance); }
.gcount {
  display: inline-flex; align-items: center; gap: 2px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: 10px;
  color: var(--muted); font: inherit; font-size: .85em; padding: 0 6px; cursor: pointer;
}
.gcount .ico { font-size: 1em; }
@media (hover: hover) { .gcount:hover { color: var(--text); border-color: var(--mixed); } }
.gcount:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
/* The pages inside an opened call-out are indented and quieted: they are the detail behind
   the row above them, and giving them the same weight would undo the grouping on screen. */
.row.grouped { background: var(--panel); font-size: .95em; }
.row.grouped .c-glyph { opacity: .75; }
@media (hover: hover) { .row.grouped:hover { background: var(--rowhover); } }

.c-glyph { grid-area: g; }
/* Wide enough for `20:03:58 gisteren`, measured at 7.10em in this font at 13px. Tabular
   figures are what keep the day starting at the same x on every row. The phone layout sizes
   this track auto and pays none of it. */
.c-time { grid-area: t; color: var(--muted); font-variant-numeric: tabular-nums; }
.c-prio { grid-area: p; }
.c-msg { grid-area: m; }

/* The location the geocoder matched, lit inside the sentence rather than cut out of it:
   moving it would risk slicing a street name in two. */
.place { color: var(--text); font-weight: 600; }
.c-msg { color: var(--text); }
/* Cleaning left nothing — the row shows what came in, dimmed, so nothing is hidden. */
.c-msg.rawbody { color: var(--muted); }

/* A road number in the colours of the board that stands along it. Literal in both themes:
   a road sign is not lit by the room it is read in. Sized in em so it rides whatever text it
   lands in. See docs/decisions/frontend.md#the-palette. */
.rw {
  font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: .01em;
  display: inline-flex; align-items: center; justify-content: center;
  white-space: nowrap; vertical-align: baseline;
  font-size: .92em; line-height: 1.25;
  padding: .08em .42em .12em; border-radius: .28em;
  border: 1.5px solid transparent;
}
.rw-a { background: #cc0605; color: #fff; border-color: rgba(255, 255, 255, .85); }
.rw-n { background: #fad201; color: #101010; border-color: rgba(0, 0, 0, .55); }

/* A second board on the road, so a pill of its own — merged in, "A16 60,1" reads as a
   four-digit road. Grey and not a sign colour: the real bord is white with black on it.
   See docs/decisions/frontend.md#the-palette. */
.hm {
  font-weight: 600; font-size: .86em; font-variant-numeric: tabular-nums;
  letter-spacing: .01em; white-space: nowrap;
  padding: .08em .34em .12em; border-radius: .25em; margin-left: .18em;
  background: rgba(128, 128, 128, .18); color: inherit;
}

/* Directe inzet: a lead-in on the text rather than a badge, since it is not an urgency and
   the priority column is one width. */
.dia {
  color: var(--muted); font-weight: 600; font-size: .82em; letter-spacing: .06em;
  margin-right: .5em;
}
/* Spelled out where there is room to learn it from, instead of an abbreviation twice. */
.diafull { color: var(--muted); font-size: .85em; margin-top: 2px; }

/* Roepnummers, clickable. A bordered chip rather than an underline: this app is used on a
   phone, where there is no hover to discover an underline with. */
/* min-width:0 and wrapping are both needed: a grid item's automatic minimum is its
   min-content and a chip is nowrap, so the roepnummers otherwise set the width of the row
   and pushed the message off the screen. */
.c-veh {
  grid-area: v; display: flex; flex-wrap: wrap; gap: 4px;
  justify-content: flex-end; min-width: 0;
}
.veh {
  font: inherit; font-size: .8em; font-variant-numeric: tabular-nums; cursor: pointer;
  padding: 0 5px; border-radius: 5px; white-space: nowrap;
  background: none; color: var(--muted);
  border: 1px solid var(--border);
}
@media (hover: hover) { .veh:hover { color: var(--police); border-color: var(--police); } }
.veh:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }

/* Something has been written about this call-out. Inside the roepnummer column, so the grid
   is untouched and it rides to the phone layout with them; `--nieuws` is the news panel's own
   token (see .omgeving.nieuws below). See docs/decisions/frontend.md#reading-a-row. */
.newsmark { display: inline-flex; align-items: center; flex: none; color: var(--nieuws); }
.newsmark .ico { font-size: 1.05em; }

/* A remembered roepnummer can be dropped from the menu, which is a different action from
   unticking it — so the row carries a second, quieter control. */
.dropforget {
  font: inherit; font-size: .95em; line-height: 1; cursor: pointer;
  background: none; border: 0; color: var(--muted); padding: 2px 4px; border-radius: 4px;
}
@media (hover: hover) { .dropforget:hover { color: var(--fire); background: var(--panel2); } }
.dropforget:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
/* In the menu the chip is a label, not a button: the checkbox beside it is what is clicked. */
.dropitem .veh.asis { cursor: inherit; pointer-events: none; }
.rawlabel { color: var(--muted); font-size: .8em; margin: 10px 0 3px; }
/* The original sits under the map: reference, not the message. Quieter than the headline
   it repeats, and never collapsed — the address helper reads a selection out of it. */
.detail .body { color: var(--muted); font-size: .92em; }
.c-msg { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cap { font-weight: 600; }
.svc { font-weight: 600; }

/* Service colors applied to capcode + svc tag + glyph */
/* The feed rows' own colouring. `.capline .cap` below deliberately overrides it: on a
   capcode row the number is no longer the thing being pointed at. */
/* .cardglyph rides along rather than restating this: a card's glyph is the row's glyph in
   another layout. */
.svc-police  .cap, .svc-police  .svc, .svc-police  .c-glyph, .svc-police  .cardglyph { color: var(--police); }
.svc-fire    .cap, .svc-fire    .svc, .svc-fire    .c-glyph, .svc-fire    .cardglyph { color: var(--fire); }
.svc-ambulance .cap, .svc-ambulance .svc, .svc-ambulance .c-glyph, .svc-ambulance .cardglyph { color: var(--ambulance); }
/* Mixed-discipline group: neutral capcode, purple MIX tag */
.svc-mixed .cap { color: var(--text); }
.svc-mixed .c-glyph, .svc-mixed .cardglyph { color: var(--mixed); }
.svc .mix, .svc.mix { color: var(--mixed); font-weight: 700; }
.mixbadge { color: #1a0b2e; background: var(--mixed); font-weight: 700; padding: 0 5px; border-radius: 4px; font-size: .8em; }

/* The other density: a card per message — the dispatch slip, set out as labelled facts.
   The discipline is a 3px rail on the left and the starred edge moves right, because two
   claims on one edge read as one thick border of an indeterminate colour.
   See docs/decisions/frontend.md#the-card-density. */
.card {
  position: relative; cursor: pointer;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--border); border-radius: 8px;
  padding: 8px 10px; margin: 0 0 6px;
}
.dens-cards { padding: 6px var(--gutter) 0; }
/* Pages inside an opened call-out stay rows and must read as the card's own detail, so the
   card gives up its bottom corners and gap while open and the rows are inset and railed. */
.dens-cards .card.expanded { margin-bottom: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.dens-cards .row.grouped { margin-left: 14px; border-left: 3px solid var(--border); }
.dens-cards .row.grouped + .card { margin-top: 6px; }
@media (hover: hover) { .card:hover { border-color: var(--muted); border-left-color: var(--fire); } }
/* The rail takes the row's discipline colours. `currentColor` is deliberately not used: a
   card drawn entirely in the discipline's colour is unreadable at 13px. */
.svc-fire.card      { border-left-color: var(--fire); }
.svc-police.card    { border-left-color: var(--police); }
.svc-ambulance.card { border-left-color: var(--ambulance); }
.svc-mixed.card     { border-left-color: var(--mixed); }
.card.selected { background: var(--sel); border-color: var(--police); }
/* On the right, because the left edge is already saying which discipline. */
.card.starred { box-shadow: inset -3px 0 0 var(--ambulance); }

.cardtop { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-bottom: 5px; }
.cardglyph { display: inline-flex; }
.cardgap { flex: 1; }
.cardwhen { color: var(--muted); font-variant-numeric: tabular-nums; }
.cardspan { color: var(--muted); font-size: .85em; white-space: nowrap; }
/* The code as transmitted, beside the badge carrying the shared scale. Dropped in script
   when the two would say the same thing. */
.cardcode {
  color: var(--muted); font-weight: 600; font-size: .82em;
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}
.carddia { color: var(--muted); font-weight: 600; font-size: .8em; letter-spacing: .06em; }
.cardmore { color: var(--muted); font-size: .82em; white-space: nowrap; }

/* Two lines and then an ellipsis: unbounded, one TESTOPROEP listing forty units is a card
   the height of the screen. */
.cardwhat {
  font-weight: 500; margin: 0 0 6px; line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.card .rawbody { color: var(--muted); }

/* The label column is fixed so values line up down the whole feed — that alignment is the
   argument for this layout. It holds the longest label, ROEPNUMMER.
   See docs/decisions/frontend.md#the-card-density. */
.cardfacts {
  display: grid; grid-template-columns: 7.4em 1fr; gap: 2px 8px;
  margin: 0; padding-top: 6px; border-top: 1px solid var(--rowline);
  font-size: .9em;
}
.cardfacts dt {
  color: var(--muted); font-size: .85em; text-transform: uppercase; letter-spacing: .05em;
  align-self: center;
}
.cardfacts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.cardwhere { color: var(--muted); }
.cardfacts dd.cardveh { display: flex; flex-wrap: wrap; gap: 4px; }
.cardfacts .gdot { margin-right: 3px; }

/* No `.active` state: neither density is the one that is "on". The glyph says which it is
   in, and that is the whole control. See docs/decisions/frontend.md#the-card-density. */
.btn.densitybtn { display: inline-flex; align-items: center; }
.btn.densitybtn .ico { font-size: 1.15em; }

/* Priority pill */
/* One width for every badge. The transparent border does the same for the outlined one,
   which would otherwise sit 2px narrower. */
.prio { font-weight: 700; padding: 0 4px; border-radius: 4px; font-size: .85em;
        border: 1px solid transparent;
        display: inline-block; min-width: 3.2em; text-align: center; }
.prio-high { color: #fff; background: var(--prio-high); }
.prio-med  { color: #201500; background: var(--prio-med); }
.prio-low  { color: #04240f; background: var(--prio-low); }
/* A0 is a tier above P1 — reanimatie or acute danger to life — but it reads the same:
   both are the loudest thing on the row, and the word REA is what separates them. */
.prio-rea  { color: #fff; background: var(--prio-high); }
/* Outlined rather than filled: a drill is not an urgency, so it should not carry the same
   visual weight as a call. Blue keeps it out of the red-amber-green scale entirely. */
.prio-test { color: var(--police); background: none; border-color: var(--police); }
/* Outlined, like TEST: filled means somebody was sent somewhere, which is the distinction
   worth seeing down the column. See docs/decisions/frontend.md#reading-a-row. */
.prio-info { color: var(--prio-info); background: none; border-color: var(--prio-info); }

/* The account link sits with the theme toggle: both are about this browser rather than
   about the feed, and neither belongs in the filter bar. */
.accountlink { text-decoration: none; display: inline-flex; align-items: center; position: relative; }
/* A dot when this browser is signed in — its absence is what tells you a sign-in did not
   stick. */
.accountlink.signed-in::after {
  content: ""; position: absolute; right: 1px; top: 3px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--prio-low); box-shadow: 0 0 0 2px var(--bg);
}

/* Loud on purpose: it is the only way a pinned app learns it is out of date, and it is
   only ever on screen when that is true. */
.updatebtn {
  font: inherit; font-size: .85em; cursor: pointer; white-space: nowrap;
  border: 1px solid var(--prio-med); border-radius: 999px; padding: 2px 10px;
  background: none; color: var(--prio-med);
}
@media (hover: hover) { .updatebtn:hover { background: var(--prio-med); color: #201500; } }
.updatebtn:focus-visible { outline: 2px solid var(--police); outline-offset: 2px; }
/* Its counterpart, shown once after the switch and gone again by itself. */
.updatednote {
  font-size: .85em; white-space: nowrap; color: var(--prio-med);
  border: 1px solid transparent; padding: 2px 10px; animation: alertpulse 1.6s ease-out;
}
@media (prefers-reduced-motion: reduce) { .updatednote { animation: none; } }
/* The same note where the release had something to say, and then it is a button: it waits to
   be read rather than fading, so it has to look like something you can press.
   See docs/decisions/frontend.md#whats-new. */
.newnote {
  font: inherit; font-size: .85em; cursor: pointer;
  border: 1px solid var(--prio-med); border-radius: 999px; background: none;
}
@media (hover: hover) { .newnote:hover { background: var(--prio-med); color: #201500; } }
.newnote:focus-visible { outline: 2px solid var(--police); outline-offset: 2px; }
@media (pointer: coarse) { .newnote { min-height: 32px; } }

/* Its panel, which is the settings panel's shape: inside the header, growing it rather than
   covering the feed. No outside-click handler for the same reason — clicking a melding must
   not throw away something that will not come back. */
.whatsnewpanel .wnhead { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.whatsnewpanel .ptitle { margin-bottom: 0; }
.wnclose {
  margin-left: auto; font: inherit; cursor: pointer; color: var(--muted);
  background: none; border: 1px solid transparent; border-radius: 7px; padding: 2px 6px;
}
@media (hover: hover) { .wnclose:hover { border-color: var(--border); color: var(--text); } }
.wnclose:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
@media (pointer: coarse) { .wnclose { min-height: 38px; min-width: 38px; } }
.wnrelease { display: flex; gap: 9px; align-items: baseline; padding: 3px 0; }
/* The version is what the note is keyed on, and it is the smaller half: somebody reads the
   lines and glances at which release they came from. */
.wnversion { flex: none; font-size: .78em; color: var(--muted); font-variant-numeric: tabular-nums; }
.wnlines { margin: 0; padding-left: 16px; font-size: .95em; }
.wnlines li { margin: 2px 0; }
/* Stacked on a phone, where a version column beside the text leaves the lines four words wide. */
@media (max-width: 520px) {
  .wnrelease { display: block; }
  .wnlines { padding-left: 15px; }
}

/* House numbers: drawn by us because the tile service will not. Small, unclickable, and
   haloed so they stay readable over a building outline or a road. */
.housenr {
  color: #1c1f24; font: 600 10px/1 system-ui, sans-serif; white-space: nowrap;
  text-shadow: 0 0 2px #fff, 0 0 2px #fff, 0 0 2px #fff;
  pointer-events: none; width: auto !important; height: auto !important;
  margin-left: -6px; margin-top: -5px;
}

/* Star / watch */
.star {
  background: none; border: none; color: var(--staroff); cursor: pointer; font-size: 14px; padding: 0; line-height: 1;
}
@media (hover: hover) { .star:hover { color: var(--ambulance); } }
.star.on { color: var(--ambulance); }
.star.big { font-size: 20px; margin-left: 8px; }

.loadolder { display: block; width: calc(100% - 24px); margin: 10px 12px; }

.empty {
  padding: 24px; color: var(--muted); text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
}

/* Detail */
.backbtn {
  display: none; width: 100%; margin: 0 0 10px; padding: 10px 12px; min-height: 44px;
  background: var(--panel2); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; font: inherit; text-align: left; cursor: pointer;
}
/* Top to bottom in the order the questions are asked: which service, how urgent, when, then
   what happened. See docs/decisions/frontend.md#the-detail-panel. */
/* The panel scrolls; this part of it does not. Its own background, because content
   passing underneath a transparent sticky block reads as a rendering fault. */
/* The sticky offset is measured from the top of the *padding box*, so the panel's own top
   padding stayed above this and everything scrolling away was visible through it. A z-index
   cannot fill a gap: the padding lives here, spanning the full width with a negative side
   margin. See docs/decisions/frontend.md#the-detail-panel. */
.dhead {
  position: sticky; top: 0; z-index: 3;
  background: var(--bg);
  margin: 0 calc(-1 * var(--gutter)) 2px;
  padding: 14px var(--gutter) 8px;
  box-shadow: 0 6px 6px -6px var(--seg-shadow);
}

.dkicker { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
/* margin-left:auto rather than a flex:1 spacer — when the row wrapped, the spacer took the
   whole first line and the buttons started the second on the left. */
.dtop { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.dtop .dacts { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: none; }
/* A link rather than a slab: sharing the line with the star and the share, a filled block
   would be the loudest thing in the panel. The 44px touch minimum stays. */
.dtop .backbtn {
  width: auto; margin: 0 0 0 -4px; padding: 8px 4px;
  background: none; border-color: transparent; color: var(--muted);
}
@media (hover: hover) { .dtop .backbtn:hover { color: var(--text); } }
.dtop .backbtn:focus-visible { outline: 2px solid var(--police); outline-offset: 1px;
                               border-radius: 6px; }
.dglyph { font-size: 20px; line-height: 1; }
.dtime { color: var(--muted); font-variant-numeric: tabular-nums; }

.detail h2.dheadline {
  margin: 0 0 6px; font-size: 20px; line-height: 1.3; font-weight: 600;
  overflow-wrap: anywhere;
}
/* Cleaning left nothing, so this is the original — dimmed, as in the feed. */
.detail h2.dheadline.rawbody { color: var(--muted); font-size: 17px; }

.dsub { color: var(--muted); display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
/* Slightly larger here than in the table below: this line sits under a 20px headline. */
.dsub .capfilter { font-size: 1em; }
.dsub .dveh { display: inline-flex; gap: 4px; }

/* One label for every block below the headline, so the panel reads as a short list of
   sections rather than as a wall. */
.dsect {
  color: var(--muted); font-size: .8em; letter-spacing: .04em;
  margin: 16px 0 6px; text-transform: uppercase;
}

/* Two buttons in the panel header: share, and the way out on layouts that have no back
   button. Same styling, different fates on a phone — see the note in the phone block. */
.dshare, .dclose {
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--muted); font: inherit; font-size: 16px; line-height: 1;
  padding: 3px 7px; cursor: pointer;
}
@media (hover: hover) {
  .dshare:hover, .dclose:hover { color: var(--text); border-color: var(--border); }
}
.dshare:focus-visible, .dclose:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }

/* A repaired reading carries no mark of its own; what was received is in the fold with the
   receiver's other numbers, and this note says so.
   See docs/decisions/flex.md#text-repair. */
.fixednote { margin: 8px 0 0; font-size: .95em; color: var(--muted); }

.dtech { margin-top: 16px; color: var(--muted); font-size: .88em; }
/* The raw message reads at full size inside the fold — it is the message, not a footnote. */
.dtech .body { font-size: 1.05em; color: var(--text); margin-top: 4px; }
.dtech .dsect { margin-top: 12px; }
.dtech .dtechrow { margin-top: 14px; }
.dtech summary { cursor: pointer; }
.dtechrow { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; font-variant-numeric: tabular-nums; }

.capcodes { margin-bottom: 12px; }
/* Two rows: the unit, then where it comes from. 60px a row against 74px before, and it
   stays 60 down to 375px — under the description instead of under the glyph it broke to 90. */
.capline {
  display: grid; grid-template-columns: 1.5em 1fr auto auto; gap: 0 8px; align-items: baseline;
  padding: 4px 9px; border: 1px solid var(--border); border-bottom: none; background: var(--panel2);
}

/* The name and where it comes from, sharing a line and wrapping together — so a row is one
   line whenever it fits, and grows only for the capcodes that genuinely have more to say. */
.capline .cmain {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; min-width: 0;
}
.capglyph {
  font: inherit; font-size: 1.05em; line-height: 1.3; background: none; border: 0; padding: 0;
  color: inherit; text-align: left;
}
button.capglyph { cursor: pointer; }
@media (hover: hover) { button.capglyph:hover { filter: brightness(1.25); } }
button.capglyph:focus-visible { outline: 2px solid var(--police); outline-offset: 2px; border-radius: 4px; }

/* Truncated rather than wrapped: a description that runs on pushes the capcode and the star
   onto a line of their own. */
.capline .what {
  font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 100%;
}
.capline .what.unknown { font-weight: 400; color: var(--muted); font-style: italic; }
/* A group address is not a unit and not an uncatalogued capcode, so it takes neither the
   solid weight of a name nor the apologetic italic of a miss. The panel's own rail colour,
   because that is what it is a property of: this page's addressing, not a responder.
   See docs/decisions/capcodes.md#group-addresses. */
.capline .what.groupcode { font-weight: 600; color: var(--mixed); font-style: normal; }

/* The star reaches out for the finger instead of standing 40px tall: .star's touch minimum
   set the height of every capcode row. The hit area is drawn outside the layout box.
   See docs/decisions/frontend.md#the-detail-panel. */
.capline .star {
  align-self: start; line-height: 1.2; position: relative;
  min-width: 0; min-height: 0; font-size: 15px;
}
.capline .star::after { content: ""; position: absolute; inset: -11px -9px; }

/* Tighter than the header's, which sits on its own line and can afford the room. Six of
   these stack up in a list that is scanned rather than read. */
.capline .capfilter { font-size: .84em; line-height: 1.45; padding: 0 5px; }
.capline .captext { font-size: .88em; }

/* The parts of the description are the filters they name, drawn as badges because hover is
   not a thing on a phone. The description itself has no filter and stays plain text. */
.capfilter {
  font: inherit; font-size: .92em; line-height: 1.5; cursor: pointer; text-align: left;
  background: none; color: inherit;
  border: 1px solid var(--border); border-radius: 5px; padding: 0 6px;
}
@media (hover: hover) {
  .capfilter:hover { border-color: var(--police); color: var(--police); }
}
.capfilter:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
.captext { color: var(--muted); }
/* The discipline, as a rail. It replaces the word "Brandweer" rather than joining it, which
   is where most of the row's height went. */
.capline { border-left: 3px solid var(--border); }
.capline.svc-fire { border-left-color: var(--fire); }
.capline.svc-ambulance { border-left-color: var(--ambulance); }
.capline.svc-police { border-left-color: var(--police); }

.capline:first-child { border-top-left-radius: 6px; border-top-right-radius: 6px; }
.capline:last-child { border-bottom: 1px solid var(--border); border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; }
/* A tint, not a second stripe: the left edge is the discipline now, and two coloured rails
   on one row is a row saying two things in the same place. */
.capline.matched { background: var(--sel); }
/* Small, muted and tabular: the numbers line up in a column that can be scanned without
   being read, which is all the capcode is needed for once the unit has a name. */
.capline .cap {
  color: var(--muted); font-size: .76em; opacity: .7; font-variant-numeric: tabular-nums;
}

.body { white-space: pre-wrap; word-break: break-word; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; margin: 0; font: inherit; }

/* Map */
.mapwrap { margin-top: 12px; }
/* isolation, so each map's panes stay inside their own stacking context — Leaflet numbers
   them 200-1000 and the map slid over the sticky header.
   See docs/decisions/frontend.md#the-palette. */
.map, .homemap, .bigmap { isolation: isolate; }
/* Height follows the width so the map keeps its shape as the pane is dragged,
   floored so it stays usable and capped so it can't push the rest off-screen. */
.map {
  /* Width from the pane, height from the width — `width: 100%` is what says which way round.
       Left at auto the ratio ran off min-height and the map laid out 384px wide on a 347px
       sheet. See docs/decisions/frontend.md#the-map. */
  width: 100%;
  aspect-ratio: 16 / 10; height: auto; min-height: 240px;
  max-height: 55vh; max-height: 55dvh;
  border-radius: 6px; border: 1px solid var(--border);
}
/* On the map rather than beside it — at full screen there is no beside. Top right, clear of
   Leaflet's zoom buttons and its attribution. */
.mapwrap { position: relative; }
.map { position: relative; }
.mapfull {
  position: absolute; top: 8px; right: 8px; z-index: 500;   /* over Leaflet's panes */
  width: 30px; height: 30px; padding: 0; line-height: 1;
  font: inherit; font-size: 15px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
}
@media (hover: hover) { .mapfull:hover { border-color: var(--muted); } }
.mapfull:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
@media (pointer: coarse) { .mapfull { width: 36px; height: 36px; font-size: 17px; } }

/* Full screen in CSS: WebKit only allows the Fullscreen API on a <video>, and a pinned app
   has no chrome to hide. See docs/decisions/frontend.md#the-map. */
.mapwrap.full {
  position: fixed; inset: 0; z-index: 1200;
  margin: 0; padding: 10px; background: var(--bg);
  display: flex; flex-direction: column; gap: 8px;
  padding-top: max(10px, env(safe-area-inset-top));
  padding-bottom: max(10px, env(safe-area-inset-bottom));
}
.mapwrap.full .map {
  flex: 1; aspect-ratio: auto; max-height: none; min-height: 0; height: auto;
}
/* The address and the OSM link stay visible: they are what the map is of. */
.mapwrap.full .maprow { flex: none; }

.mapnote { color: var(--muted); padding: 6px 2px; font-size: .9em; }
.mapnote.loading { color: var(--police); }
.mapnote.error { color: var(--prio-med); }
/* Said in words as well as drawn as a circle. Amber rather than red: a coarse pin is not an
   error, it is the message not naming a street. */
.precisionnote {
  color: var(--prio-med); font-size: .85em; padding: 4px 2px;
  display: flex; align-items: baseline; gap: 6px;
}

/* Stacked, not shared: as one flex row the address got a column two words wide beside two
   things that refuse to wrap. */
.maprow { display: flex; flex-direction: column; gap: 4px; padding: 6px 2px; }
/* The address is the link out, and the arrow carries the affordance — without it a coloured
   address reads as a filter. See docs/decisions/frontend.md#the-surroundings-panel. */
.mapaddr { display: inline-flex; align-items: baseline; gap: 5px; color: var(--muted);
           font-size: .85em; word-break: break-word; text-decoration: none; }
.mapaddr .ext { color: var(--police); flex: none; font-size: .95em; }
@media (hover: hover) {
  .mapaddr:hover .t { text-decoration: underline; }
  .mapaddr:hover .ext { text-decoration: none; }
}
.mapaddr:focus-visible { outline: 2px solid var(--police); outline-offset: 2px; border-radius: 4px; }
.mapctl { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.mapzoom { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: .85em; white-space: nowrap; }
/* Leaflet renders dark controls fine, but keep attribution readable on dark */
/* Light in both themes: the container's colour shows only where PDOK's pastel sheet is
   transparent, so a dark ground made the North Sea black around a white Netherlands.
   See docs/decisions/frontend.md#the-palette. */
.leaflet-container { background: #e8eaed; }

/* Address helpers */
.btn.tiny { padding: 2px 7px; font-size: .85em; }
.hintactions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 2px 0; }
.hinthelp { color: var(--muted); font-size: .85em; }
.hintbar {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 6px 8px; margin-top: 6px; font-size: .85em;
  background: var(--panel2); border: 1px solid var(--border); border-radius: 6px;
}
.hinttext { flex: 1; min-width: 0; word-break: break-word; }
.hintsnip { color: var(--police); background: var(--sel); border-radius: 4px; padding: 1px 5px; }
.hintpin { color: var(--ambulance); }
.hintwarn { color: var(--prio-med); font-size: .85em; padding: 6px 2px; }
.pinbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 6px; padding: 7px 9px; font-size: .85em; color: var(--ambulance);
  background: var(--panel2); border: 1px solid var(--ambulance); border-radius: 6px;
}
.pincoords { color: var(--muted); }

/* One-time "add to home screen" tip. Sits above the home indicator, below the modal. */
.installtip {
  position: fixed; z-index: 980; left: 10px; right: 10px;
  bottom: calc(10px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 14px; background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, .45);
}
.installicon { width: 44px; height: 44px; border-radius: 10px; flex: none; }
.installtext { flex: 1; min-width: 190px; display: flex; flex-direction: column; gap: 3px; }
.installtext b { color: var(--text); }
.installtext span { color: var(--muted); font-size: .92em; line-height: 1.45; }
.installactions { display: flex; gap: 8px; margin-left: auto; }

/* Address-helper popup */
.modal {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(0, 0, 0, .55);
}
.dialog {
  width: min(460px, 100%); max-height: 100%; overflow: auto;
  padding: 16px 18px; background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, .6);
}
.dialog h3 { margin: 0 0 6px; font-size: 1.05em; }

/* ---- The manual ------------------------------------------------------------------- */
/* A column, so the prose scrolls between a heading and a footer that stay put: a close
   button that scrolls away in a long document is a small cruelty. */
.dialog.manual {
  width: min(760px, 100%); display: flex; flex-direction: column;
  max-height: min(88vh, 100%); padding: 0;
}
.manualhead, .manualfoot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 18px; flex: none;
}
.manualhead { border-bottom: 1px solid var(--border); }
.manualfoot { border-top: 1px solid var(--border); }
.manualbody { overflow-y: auto; padding: 4px 18px 14px; }

/* Phone: the manual is the screen, not a card on it. Safe-area insets go on the bar meeting
   each edge, so the heading clears the notch and the close button the home indicator. */
@media (max-width: 760px) and (pointer: coarse) {
  .modal { padding: 0; }
  .dialog.manual {
    width: 100%; height: 100%; max-height: none;
    border: none; border-radius: 0; box-shadow: none;
  }
  .manualhead { padding-top: max(14px, env(safe-area-inset-top)); }
  .manualfoot { padding-bottom: max(14px, env(safe-area-inset-bottom)); }
  .manualhead, .manualfoot, .manualbody {
    padding-left: max(18px, env(safe-area-inset-left));
    padding-right: max(18px, env(safe-area-inset-right));
  }
}
/* Prose measure: much past this and the eye loses the start of the next line. */
.manualbody p, .manualbody ul { max-width: 62ch; }
.manualbody h4 {
  margin: 20px 0 6px; font-size: .98em; color: var(--text);
}
.manualbody h4.manualadv {
  margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--border);
}
.manualbody p { margin: 0 0 10px; line-height: 1.6; color: var(--muted); }
/* The name itself, above everything the app does. An aside rather than a heading: it is
   the first thing a curious reader wants and the last thing a hurried one needs. */
/* Drawn in white strokes, like the one in the header, so a light background inverts it
   there and here for the same reason. */
.manuallogo { height: 26px; width: auto; display: block; margin: 6px 0 12px; }
:root[data-theme="light"] .manuallogo { filter: invert(1); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .manuallogo { filter: invert(1); }
}
.manualname { font-size: .92em; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.manualname a { color: var(--police); }
.manualbody b { color: var(--text); font-weight: 600; }
.manualbody ul { margin: 0 0 12px; padding-left: 20px; line-height: 1.6; color: var(--muted); }
.manualbody li { margin-bottom: 5px; }
.manualbody code {
  background: var(--panel2); border: 1px solid var(--border); border-radius: 4px;
  padding: 0 4px; font-size: .9em; color: var(--text);
}
.dialognote { margin: 0 0 14px; color: var(--muted); font-size: .85em; line-height: 1.5; }
/* Why the locatieknop cannot work, or why it just didn't: it answers the button above it,
   so it sits against it rather than in the dialog's own rhythm. */
.gpsnote { margin: -2px 0 10px; font-size: .85em; line-height: 1.5; }
.drow { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
.dlabel { color: var(--muted); width: 72px; flex: none; font-size: .85em; }
.drow input {
  flex: 1; min-width: 0; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; font: inherit;
}
.drow input:focus { outline: none; border-color: var(--police); }
.dpin { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: var(--ambulance); font-size: .85em; }
.dmuted { color: var(--muted); font-size: .85em; }
.dscope { display: flex; gap: 14px; flex-wrap: wrap; font-size: .85em; }
.dscope label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
.hintscope { color: var(--muted); }
.dresults {
  display: flex; flex-direction: column; gap: 4px;
  margin: 6px 0 2px 82px; max-height: 220px; overflow-y: auto;
}
.dresult {
  display: flex; flex-direction: column; gap: 2px; width: 100%;
  padding: 6px 9px; text-align: left; cursor: pointer; font: inherit;
  background: var(--panel2); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
}
@media (hover: hover) { .dresult:hover { border-color: var(--police); } }
.rhead { display: flex; align-items: baseline; gap: 8px; }
.rname { color: var(--text); }
.rkind {
  color: var(--ambulance); font-size: .78em; text-transform: lowercase;
  border: 1px solid var(--border); border-radius: 4px; padding: 0 5px;
}
.rdisplay { color: var(--muted); font-size: .78em; line-height: 1.35; }
.dactions { display: flex; align-items: center; gap: 10px; padding-top: 14px; }
.btn.primary { border-color: var(--police); color: var(--police); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn:disabled:hover { border-color: var(--border); }
.leaflet-control-attribution { background: rgba(0,0,0,.6); color: #9aa4af; }
.leaflet-control-attribution a { color: #c0c8d0; }

/* ---------------------------------------------------------------------------
   Responsive
   --------------------------------------------------------------------------- */

/* Tablet / narrow window: stack the detail pane, drop the service column. */
@media (max-width: 760px) {
  .layout { flex-direction: column; }
  .detail { width: auto; border-left: none; border-top: 1px solid var(--border); max-height: 45vh; max-height: 45dvh; }
  .feedhead, .row {
    grid-template-columns: 1.8em 7.3em 3.4em 1fr auto;
    grid-template-areas: "g t p m v";
  }
}

/* Phone: the detail pane becomes a full-screen sheet. Splitting an already short
   screen leaves both halves unusable, and the map needs the room. */
@media (max-width: 760px) and (pointer: coarse) {
  .splitter { display: none; }
  .detail {
    position: fixed; inset: 0; z-index: 950;
    width: auto !important;              /* beats the dragged inline width */
    max-height: none; border-top: none; overflow-y: auto;
    padding-top: 0;                      /* .dhead carries it — see the note there */
    padding-bottom: max(14px, env(safe-area-inset-bottom));
    padding-left: max(var(--gutter), env(safe-area-inset-left));
    padding-right: max(var(--gutter), env(safe-area-inset-right));
  }
  /* The notch inset moves with it, and the header spans the sheet's full width so the
     background it paints reaches both edges. */
  .dhead {
    padding-top: max(14px, env(safe-area-inset-top));
    margin-left: calc(-1 * max(var(--gutter), env(safe-area-inset-left)));
    margin-right: calc(-1 * max(var(--gutter), env(safe-area-inset-right)));
    padding-left: max(var(--gutter), env(safe-area-inset-left));
    padding-right: max(var(--gutter), env(safe-area-inset-right));
  }
  .backbtn { display: block; }
  /* Only the close button is redundant: the sheet has a full-width back button. Sharing has
       no other route on a pinned app — no address bar, no browser menu — so it stays. */
  .dclose { display: none; }
  .dshare { font-size: 20px; padding: 6px 10px; }
  .map { max-height: none; }             /* the sheet scrolls; let it be square-ish */
}

/* Narrow phone: a card per message. Six columns at 390px squeezes the one field
   that matters — the message — down to nothing. */
@media (max-width: 560px) {
  .feedhead { display: none; }           /* column headings mean nothing to cards */
  .row {
    /* Time and priority on the first line, the message below it, with the
       roepnummers taking the free column so they close the row on the right. */
    /* Priority hard right: it is read down the column at a glance, so it wants a fixed edge.
           The vehicles take the slack. */
    grid-template-columns: 1.7em auto 1fr auto;
    grid-template-areas:
      "g t v p"
      "m m m m";
    gap: 3px 8px; align-items: center; padding: 8px var(--gutter);
  }
  .c-prio { justify-self: end; }
  .c-msg {
    white-space: normal; overflow: hidden; text-overflow: clip;
    overflow-wrap: anywhere; padding: 2px 0;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  }
  /* Tight, because every pixel between two buttons is a pixel that decides whether the
     row wraps. */
  .toolbar { flex-wrap: wrap; gap: 4px; }
  /* The search shares the row with the buttons; a row to itself costs 48px of feed.
       A basis, not `auto`: the browser decides whether to wrap from the basis, and auto
       measures the placeholder at full width, so at 320px the row broke while the field was
       willing to shrink. 160px is what it asks for; it grows into what the buttons leave. */
  .filterbox { flex: 1 1 160px; max-width: none; min-width: 0; }
  .count, .spacer { display: none; }
  .statusdetail { gap: 10px 12px; font-size: .95em; }

  /* Icon-only toolbar: five labelled buttons do not fit on a phone. The exception is a
     bewaard filter's name, which is the one label that says what you are looking at. */
  .btnlabel { display: none; }
  .btn.filterbtn.haspreset .btnlabel { display: inline; max-width: 9ch; }
  .btnbadge {                      /* keeps an active region filter countable */
    display: inline; margin-left: 5px; font-weight: 700;
    color: var(--prio-med);
  }
  .toolbar .btn { padding-left: 10px; padding-right: 10px; }
  .kindbtns { gap: 6px; }
}

/* Touch: iOS zooms any input under 16px on focus and never zooms back, and Apple's
   minimum comfortable target is 44px. */
@media (pointer: coarse) {
  /* Every field on every page, not the two somebody remembered: the body font is 13px, so a
       field that does not say otherwise inherits iOS's zoom trigger and the zoom is not undone
       when the field is left. Ranges, checkboxes and radios take no text. !important because
       account.css and admin.css load after this file.
       See docs/decisions/frontend.md#the-shell. */
  input:not([type="range"]):not([type="checkbox"]):not([type="radio"]),
  textarea, select { font-size: 16px !important; }
  .btn { padding: 9px 13px; min-height: 44px; }
  .btn.tiny { padding: 7px 11px; min-height: 38px; font-size: .9em; }
  .star { font-size: 17px; min-width: 40px; min-height: 40px; }
  .star.big { font-size: 22px; }
  .dropitem { min-height: 44px; }
  .hintbtn { width: 100%; text-align: center; }
  .rows { overscroll-behavior: contain; }  /* don't bounce the whole page */
  .dialog { padding-bottom: max(16px, env(safe-area-inset-bottom)); }
}

.homemap {
  height: 200px; margin: 12px 0 4px;
  border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
}
/* The popup scrolls; the map must not steal the wheel while it does. */
.homemap .leaflet-container { background: #e8eaed; }   /* the same pastel sheet, so the same paper */

/* "Near me": the filter and its settings gear read as one control. */
.btn.nearbtn .btnicon { color: var(--police); }
.nearswitch { align-items: center; }
.nearswitch .btn { white-space: nowrap; }
.rangein { flex: 1; min-width: 0; accent-color: var(--police); }
.rangeval { color: var(--text); font-variant-numeric: tabular-nums; width: 48px; text-align: right; }
.dactions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; }

/* The kazerne under a post's name: a second line in the same cell rather than a column of
   its own, because most posts do not have one and an empty column reads as a broken table.
   See docs/decisions/geocoding.md#the-drill-down-keeps-its-own-rows. */
.postkaz { display: block; color: var(--muted); font-size: .82em; line-height: 1.35; }

/* Station grouping and observed pairings */
.stationbtn { margin-left: 6px; }
/* One more column than the list above: the count is what this list is for. */
.related-line { grid-template-columns: 1.5em 1fr auto auto auto; }
.relcount { color: var(--muted); font-variant-numeric: tabular-nums; font-size: .85em; }

/* A station is entered from a message and only ever left, so it is announced on the list it
   narrows rather than sitting among the toggles. Outside .rows, so it stays while it
   scrolls. */
.stationbanner {
  display: flex; align-items: center; gap: 8px;
  padding: 7px var(--gutter); background: var(--panel2);
  border-bottom: 1px solid var(--border); box-shadow: inset 3px 0 0 var(--fire);
  font-size: .9em;
}
.sbicon { flex: none; }
.sbtext { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sbsvc, .sbregion { color: var(--muted); }
@media (max-width: 640px) { .sbregion { display: none; } }

/* ---- View switcher and the non-feed views ---- */
/* One segmented control that changes position, not two components: same markup, same
   styling, one `order`. A thumb reaches the bottom of a phone and a cursor is already at the
   top of a window. */
.viewnav {
  flex: none; display: flex;
  padding: 6px var(--gutter) 0; background: var(--bg);
}
.viewseg {
  display: flex; gap: 2px; padding: 3px;
  background: var(--seg-track); border-radius: 11px;
}
.viewbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 14px; min-height: 36px; cursor: pointer;
  background: none; color: var(--muted); font: inherit; font-size: 14px;
  border: 0; border-radius: 9px;
}
@media (hover: hover) { .viewbtn:hover { color: var(--text); } }
.viewbtn.active {
  color: var(--text); background: var(--seg-active);
  box-shadow: 0 1px 2px var(--seg-shadow);
}
.viewbtn:focus-visible { outline: 2px solid var(--police); outline-offset: -2px; }
@media (prefers-reduced-motion: no-preference) {
  .viewbtn { transition: color .12s ease, background-color .12s ease; }
}

/* On a wide window the track was the widest block of colour on a light screen, so the
   buttons carry the state on their own. The transparent border keeps them the same width
   whichever is selected. */
@media (min-width: 641px) {
  .viewseg { background: var(--seg-track-wide); padding: 0; gap: 4px; }
  .viewbtn { border: 1px solid transparent; }
  .viewbtn.active { background: var(--seg-active-wide); border-color: var(--seg-edge-wide); }
}

.viewpane { flex: 1; display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.viewpane.scrolls { overflow-y: auto; padding: 12px var(--gutter); gap: 12px; }
.viewnote {
  padding: 7px var(--gutter); color: var(--muted); font-size: .85em;
  border-bottom: 1px solid var(--border); display: flex; gap: 8px; flex-wrap: wrap;
}
/* Only the cells too small to hold their own number need the key, so it is a swatch on the
   note's line rather than a box over the map.
   See docs/decisions/frontend.md#density-grid. */
.densitykey { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.densitykey i { width: 46px; height: 8px; border-radius: 4px; display: inline-block; }

.bigmap { flex: 1; min-height: 0; }

.panel {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 10px; padding: 12px 14px;
}
.panel h3 {
  margin: 0 0 10px; font-size: 1em; display: flex; align-items: center; gap: 10px;
}
.crumb { color: var(--muted); font-weight: normal; }

.kindbars { display: flex; flex-direction: column; gap: 6px; }
.kindbar { display: flex; align-items: center; gap: 10px; font-size: .9em; }
.kindname { width: 110px; flex: none; color: var(--muted); }
.kindname.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.bartrack { flex: 1; min-width: 0; height: 14px; background: var(--panel2); border-radius: 7px; overflow: hidden; }
.barfill { display: block; height: 100%; background: var(--muted); border-radius: 7px; }
.barfill.k-fire { background: var(--fire); }
.barfill.k-ambulance { background: var(--ambulance); }
.barfill.k-police { background: var(--police); }
.barval { width: 64px; text-align: right; font-variant-numeric: tabular-nums; }

.hourchart { display: flex; align-items: flex-end; gap: 3px; height: 168px; }
.hourcol {
  flex: 1; min-width: 0; display: flex; flex-direction: column;
  justify-content: flex-end; align-items: center; height: 100%;
  background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer;
}
.hourbar { width: 100%; background: var(--police); border-radius: 3px 3px 0 0; min-height: 2px; }
.hourlab { color: var(--muted); font-size: .7em; padding-top: 3px; font-variant-numeric: tabular-nums; }
/* The count above its own bar. Small enough that 24 of them fit, and blank on an empty
   hour rather than a row of zeroes. */
.hourval {
  color: var(--muted); font-size: .68em; line-height: 1.2; padding-bottom: 2px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
@media (hover: hover) { .hourcol:hover .hourbar { background: var(--prio-med); } }
.hourcol.picked .hourbar { background: var(--prio-med); }
.hourcol.picked .hourval, .hourcol.picked .hourlab { color: var(--text); }
.hourcol:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; border-radius: 3px; }
.hourpicked { margin: 8px 0 0; color: var(--muted); display: flex; align-items: center;
              gap: 8px; flex-wrap: wrap; }
.hourpicked b { color: var(--text); }

/* A bar that is a button: the row keeps the layout it had as a div and gives up only its
   background, so the chart does not become a list of controls. Reset explicitly — a <button>
   brings its own font, padding and border. */
.pickbar { font: inherit; color: inherit; background: none; border: 0; padding: 3px 4px;
           margin: 0; width: 100%; text-align: left; cursor: pointer; border-radius: 7px; }
@media (hover: hover) {
  .pickbar:not([disabled]):hover { background: var(--panel2); }
  .pickbar:not([disabled]):hover .bartrack { background: var(--border); }
}
.pickbar:focus-visible { outline: 2px solid var(--police); outline-offset: 1px; }
.pickbar.picked { background: var(--panel2); box-shadow: inset 0 0 0 1px var(--border); }
.pickbar.picked .kindname { color: var(--text); font-weight: 600; }
/* "Geen" is a reading and not a filter: the reference has nothing to match on, so the row
   is drawn and says so by not lifting under the cursor. */
.pickbar[disabled] { cursor: default; opacity: .75; }

.barfill.p-rea { background: var(--prio-high); }
.barfill.p-high { background: var(--prio-high); }
.barfill.p-med { background: var(--prio-med); }
.barfill.p-low { background: var(--prio-low); }
.barfill.p-test { background: var(--police); }
.barfill.p-none { background: var(--prio-info); }

/* The biggest call-outs. The count is what the list is ordered by, so it leads, in the
   discipline-neutral --prio-med the hour bars use. */
.biglist { display: flex; flex-direction: column; gap: 2px; }
.bigrow { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
          font: inherit; color: inherit; background: none; border: 0; cursor: pointer;
          padding: 7px 6px; border-radius: 7px; }
.bigrow + .bigrow { border-top: 1px solid var(--border); }
@media (hover: hover) { .bigrow:hover { background: var(--panel2); } }
.bigrow:focus-visible { outline: 2px solid var(--police); outline-offset: -2px; }
.bigcount { flex: none; width: 46px; text-align: right; font-size: 1.35em; font-weight: 700;
            color: var(--prio-med); font-variant-numeric: tabular-nums; line-height: 1.1; }
.bigcount small { display: block; font-size: .38em; font-weight: 500; letter-spacing: .06em;
                  text-transform: uppercase; color: var(--muted); }
.bigtext { min-width: 0; flex: 1; }
/* One line, clipped. The headline is the longest page of the call-out, which is often a
   paragraph, and a list whose rows are three lines deep stops being scannable. */
.bigmsg { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bigwhen { display: block; font-size: .82em; color: var(--muted); margin-top: 1px; }

.clickrow { cursor: pointer; }
@media (hover: hover) { .clickrow:hover { background: var(--panel2); } }

@media (max-width: 640px) {
  /* Last in the column. The popups are position:fixed and out of flow, so the only other
     flex children are the header, the toolbar and whichever view is showing. */
  .viewnav {
    order: 10;
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
    border-top: 1px solid var(--border);
  }
  .viewseg { flex: 1; }
  /* Icon over label rather than beside it: four segments have to fit a narrow phone, and
     stacking buys the width without having to drop the labels, which the old top tabs did. */
  .viewbtn {
    flex: 1; min-width: 0; flex-direction: column; gap: 2px;
    padding: 5px 2px 6px; min-height: 44px; font-size: 10.5px;
  }
  .viewicon { font-size: 15px; line-height: 1; }
  .viewlabel { display: block; }
  /* Twenty-four numbers do not fit across a phone; tapping a bar still reads one out. */
  .hourval { display: none; }
  .kindname { width: 80px; }
  .barval { width: 52px; }
  .hourlab { font-size: .6em; }
}

/* Time range */
.customrange { padding: 6px 10px 10px; border-top: 1px solid var(--border); }
.customrange .drow { gap: 8px; }
.customrange input {
  flex: 1; min-width: 0; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px; font: inherit;
}
.emptyrange {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 18px 14px; color: var(--muted);
}
.emptyrange b { color: var(--text); }

/* Disciplines, collapsed into a menu. Same machinery as the regio menu; the colour stays on
   the item so the list still reads at a glance. */
.kindmenu { min-width: 12rem; }
.dropglyph { margin-right: 2px; }
.kindmenu .dropitem.k-fire .dropname { color: var(--fire); }
.kindmenu .dropitem.k-ambulance .dropname { color: var(--ambulance); }
.kindmenu .dropitem.k-police .dropname { color: var(--police); }

/* The feedback box. Wide enough that a sentence does not wrap after four words, and
   resizable vertically because some people write more than others. */
.feedbacktext {
  width: 100%; min-height: 6em; font: inherit; font-size: .95em; line-height: 1.5;
  padding: 8px 10px; resize: vertical;
  background: var(--panel2); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
}
.feedbacktext:focus { border-color: var(--police); outline: none; }
.dialognote.bad { color: var(--prio-high); }

/* Omgeving: what OpenStreetMap says stands along this street. The chips reuse .fopt because
   they are filter chips and answer the same kind of question.
   See docs/decisions/frontend.md#the-surroundings-panel. */
.omgbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 0; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 7px; overflow: hidden; }
.seg button { font: inherit; font-size: .85em; padding: 5px 11px; cursor: pointer;
              background: var(--panel); color: var(--muted); border: 0;
              border-right: 1px solid var(--border); }
.seg button:last-child { border-right: 0; }
.seg button[aria-pressed="true"] { background: var(--sel); color: var(--text); font-weight: 600; }

.kindbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 8px 0 0; }
.kindbar .fopt { font-size: .85em; }
.kindbar .count { color: var(--muted); font-size: .8em; margin-left: 4px; }

.omgeving { border-left: 3px solid var(--prio-low); background: var(--panel2);
            border-radius: 0 8px 8px 0; padding: 12px 14px; margin: 12px 0; }
.omgtitle { font-size: .78em; letter-spacing: .09em; text-transform: uppercase;
            color: var(--prio-low); margin-bottom: 8px; }
.roadline { font-size: 1.02em; margin-bottom: 10px; }
.omglist { display: flex; flex-direction: column; gap: 2px; }
.omgrow { display: flex; align-items: baseline; gap: 9px; padding: 3px 0; }
.omgrow .g { width: 1.4em; flex: none; }
.omgrow .n { flex: 1; min-width: 0; }
.omgrow .w { color: var(--muted); font-size: .85em; white-space: nowrap; }
/* Dimmed rather than hidden: on "alles" the reader asked to see what is not relevant to
   this discipline, and dropping the distinction would make the mode meaningless. */
.omgrow.dim { opacity: .5; }
.omgfoot { color: var(--muted); font-size: .8em; margin-top: 10px;
           border-top: 1px solid var(--border); padding-top: 8px; }

/* ── The map's own markers ───────────────────────────────────────────────────────────
   Three silhouettes, and the silhouette is what carries the meaning: a teardrop is an
   event at a point, a square is a building, a circle is something that merely stands
   there. Colour says which service, and it cannot say more than that — the incident is
   fixed at red and the brandweer is red too, so on colour alone a kazerne and a melding
   would be the same marker. See docs/decisions/frontend.md#the-map.

   Every one of these is drawn on the basemap, which is **PDOK pastel in both themes** —
   there is no dark counterpart. So the ink and the rims here are literal rather than
   theme tokens, in the way the road badges are: what a marker sits on does not change
   when the room does. */

/* The incident. Red is `--prio-high`'s hue held still: a token would move between the
   themes, and this red is the one colour on the map that means exactly one thing. */
:root { --incident: #d4131b; }

.pin { width: 32px; height: 32px; position: relative;
       filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .35)); }
/* The teardrop: a circle with one square corner, turned so the corner points down. The
   tip is the spot — iconAnchor puts it exactly on the coordinate. */
.pin .pinbody { position: absolute; inset: 0; background: var(--incident);
                border: 2.5px solid #fff; border-radius: 50% 50% 50% 0;
                transform: rotate(-45deg); }
.pin .pinico { position: absolute; inset: 0; display: grid; place-items: center; }
.pin .pinico .ico { width: 16px; height: 16px; color: #fff; }
/* Where the reader lives, in the colour of the circle it stands in. Not an incident. */
.pin.home { width: 30px; height: 30px; }
.pin.home .pinbody { background: #2f6fd0; }
.pin.home .pinico .ico { width: 14px; height: 14px; }

/* A post: square, because it is a building. Never a pin — it does not mark a spot the
   way the incident does, and the difference has to survive 26px and greyscale. */
.post { width: 26px; height: 26px; border-radius: 6px;
        display: grid; place-items: center;
        border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .35); }
.post .ico { width: 15px; height: 15px; color: #fff; }
/* Spelled out per discipline rather than through a custom property, so a kind with no
   colour cannot silently draw a transparent square. */
.post.svc-fire      { background: var(--fire); }
.post.svc-police    { background: var(--police); }
.post.svc-ambulance { background: var(--ambulance); }
.post.svc-other     { background: #6b7480; }

/* The way from a post to the incident. A white casing under a coloured line, or it vanishes
   into the salmon that the pastel basemap draws its roads in — the same reason the markers
   carry a white rim. Coloured through CSS rather than through a Leaflet option, so the line
   and the square it starts at are the one colour and follow the theme together.
   See docs/decisions/geocoding.md#the-route-from-the-post. */
.routecase { stroke: #fff; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round;
             opacity: .9; fill: none; }
.routeline { stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round;
             opacity: .95; fill: none; stroke: currentColor; color: #6b7480; }
.routeline.svc-fire      { color: var(--fire); }
.routeline.svc-police    { color: var(--police); }
.routeline.svc-ambulance { color: var(--ambulance); }

/* Which end is the incident, said on the line itself. Chevrons and not one arrowhead: an
   arrowhead at the end sits under the speld, and one in the middle is gone the moment
   somebody pans. */
.chev { display: block; width: 12px; height: 12px; overflow: visible;
        transform: rotate(var(--a)); color: #6b7480; }
.chev path { fill: currentColor; stroke: #fff; stroke-width: 1.2; paint-order: stroke; }
.chev.svc-fire      { color: var(--fire); }
.chev.svc-police    { color: var(--police); }
.chev.svc-ambulance { color: var(--ambulance); }

/* What merely stands there: a circle, and neutral. **Literal chrome and not `--panel`**,
   for the reason above — the tiles under it are pale in both themes, so a panel-coloured
   disc was a dark blob on a light map for every reader on the dark theme. */
.poi { display: flex; align-items: center; justify-content: center;
       width: 24px; height: 24px; font-size: 15px;
       background: #fbfbfa; border: 1px solid #b9bfc7; border-radius: 50%;
       color: #171c23;
       box-shadow: 0 1px 3px rgba(0, 0, 0, .35); }

/* Waiting and failing look like the panel they will become. Grey rail rather than green:
   nothing is established yet. */
/* A sibling of the surroundings, not a variant: same rows, different source, and the rail
   says which. */
.omgeving.weer { border-left-color: var(--police); }
/* A heading inside the list rather than a second panel: it is the same reading, split into
   what the weather is and what that means for what is around. */
.omgsub { color: var(--muted); font-size: .78em; letter-spacing: .06em; text-transform: uppercase;
          margin: 8px 0 2px; padding-top: 7px; border-top: 1px solid var(--rowline); }
.omgeving.weer .omgtitle { color: var(--police); }

/* The two capcode lists are panels of the same shape. Inside one, a row gives up its border,
   fill and dimming, and the discipline rail goes with the border — a rail inside a rail reads
   as two panel edges. The heading moves into the block, in the rail's colour.
   See docs/decisions/frontend.md#the-detail-panel. */
/* Same panel shape, in the purple the capcodes carry: both blocks answer "what else went out
   for this", one by address and one by page. */
.omgeving.samen { border-left-color: var(--mixed); margin: 12px 0; }
.omgeving.samen .omgtitle { color: var(--mixed); }
.capline.sameline {
  grid-template-columns: auto auto 1fr auto; gap: 8px; cursor: pointer;
}
.capline.sameline .samewhen { color: var(--muted); font-size: .85em; font-variant-numeric: tabular-nums; }
.capline.sameline .samewhat { min-width: 0; overflow-wrap: anywhere; }
@media (hover: hover) { .capline.sameline:hover { background: var(--rowhover); } }

.omgeving.capcodes { border-left-color: var(--mixed); margin: 12px 0; }
.omgeving.capcodes .omgtitle { color: var(--mixed); }
.omgeving.related { border-left-color: var(--related); margin: 16px 0 12px; }
.omgeving.related .omgtitle { color: var(--related); }

.omgeving .capline {
  border: 0; border-radius: 5px; background: none; opacity: 1;
  padding: 3px 8px; margin: 0 -8px;
}
/* --border rather than --rowline: --rowline is drawn on --bg in the feed, and on the panel's
   own --panel2 it is the darker of the two in the dark theme. */
.omgeving .capline + .capline { border-top: 1px solid var(--border); border-radius: 0; }
/* The one row that still earns a fill: the capcode this message matched a filter on. */
.omgeving .capline.matched { background: var(--sel); }

/* The place: map, address and what stands along the street, in one panel. The address is the
   heading and is a name, so it is not set in capitals like the others. */
/* The register: the question and its answer, one rail because they are one source. */
.omgeving.bagvraag, .omgeving.bag { border-left-color: var(--bag); margin: 12px 0; }
.omgeving.bagvraag .omgtitle, .omgeving.bag .omgtitle { color: var(--bag); }
.omgeving .omgrow .n.flag { color: var(--prio-med); }

.nrform { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.nrform label { font-size: .85em; color: var(--muted); }
.nrform input { font: inherit; width: 7em; background: var(--panel); color: var(--text);
                border: 1px solid var(--border); border-radius: 6px; padding: 6px 9px; }
.btn.bagbtn { background: var(--bag); border-color: var(--bag); color: var(--bg);
              font-weight: 600; }
.baghint { color: var(--muted); font-size: .82em; margin-top: 8px; }

.picklist { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
.pick { display: flex; align-items: baseline; gap: 9px; text-align: left; width: 100%;
        font: inherit; background: none; border: 0; color: var(--text); cursor: pointer;
        padding: 5px 8px; margin: 0 -8px; border-radius: 5px; }
.pick + .pick { border-top: 1px solid var(--border); border-radius: 0; }
.pick .h { font-weight: 600; flex: none; min-width: 3.6em; }
.pick .d { color: var(--muted); font-size: .87em; flex: 1; min-width: 0; }
@media (hover: hover) { .pick:hover { background: var(--sel); } }

/* The building itself. Its own box, the same shape the map's is, because it is the same kind
   of object: a picture of this place from another direction. */
.pandbox { margin-top: 10px; background: var(--panel); border: 1px solid var(--border);
           border-radius: 8px; overflow: hidden; }
.pandstage { position: relative; aspect-ratio: 16 / 11; }
/* pan-y, not none: the canvas is most of a phone's screen when this panel is open, and one
   that swallows every gesture is a hole you cannot scroll past. Sideways turns the building.
   See docs/decisions/frontend.md#the-building. */
.pandstage canvas { display: block; width: 100%; height: 100%; touch-action: pan-y;
                    cursor: grab; }
.pandstage canvas:active { cursor: grabbing; }
.pandchrome, .pandnorth { position: absolute; pointer-events: none; }
.pandchrome { inset: 0; width: 100%; height: 100%; }
.pandbar { stroke: var(--muted); stroke-width: 1.5; opacity: .8; }
.pandnorth { right: 10px; top: 10px; width: 38px; height: 38px; }
.pandnorth circle { fill: none; stroke: var(--border); stroke-width: 1.5; }
.pandnorth line { stroke: var(--fire); stroke-width: 3; stroke-linecap: round; }
.pandbarlabel, .pandnorthlabel { position: absolute; color: var(--muted); font-size: .72em;
                                 pointer-events: none; }
.pandbarlabel { left: 10px; bottom: 4px; }
.pandnorthlabel { right: 10px; top: 50px; width: 38px; text-align: center; }
.pandfoot { display: flex; align-items: center; justify-content: space-between; gap: 10px;
            flex-wrap: wrap; color: var(--muted); font-size: .78em; padding: 6px 10px;
            border-top: 1px solid var(--border); }

.bagfoot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
           color: var(--muted); font-size: .8em; margin-top: 10px;
           border-top: 1px solid var(--border); padding-top: 8px; }

.omgeving.plaats { border-left-color: var(--prio-low); margin: 12px 0; }
.omgeving.plaats > .omgtitle { color: var(--prio-low); }
a.omgtitle.addr {
  display: flex; align-items: baseline; gap: 6px;
  text-transform: none; letter-spacing: 0; font-size: 1.02em;
  text-decoration: none; margin-bottom: 10px;
}
a.omgtitle.addr .t { min-width: 0; overflow-wrap: anywhere; }
a.omgtitle.addr .ext { color: var(--muted); font-size: .85em; flex: none; }
@media (hover: hover) {
  a.omgtitle.addr:hover .t { text-decoration: underline; }
  a.omgtitle.addr:hover .ext { color: var(--prio-low); }
}
a.omgtitle.addr:focus-visible { outline: 2px solid var(--police); outline-offset: 2px;
                                border-radius: 4px; }
/* The map sits directly under the heading; the controls stay under the map; what is along
   the street follows, separated the way .omgsub separates the downwind from the weather. */
.omgeving.plaats .mapwrap { margin-top: 0; }
.omgeving.plaats .mapctl { display: flex; align-items: center; gap: 10px;
                           flex-wrap: wrap; padding: 6px 0 0; }
.omgeving.plaats .roadline { margin: 12px 0 6px; padding-top: 10px;
                             border-top: 1px solid var(--border); }
/* Loading, failed and vague, said inside the panel. Same words as the .omgfoot they
   replaced, so nothing appears from nowhere once there is an answer. */
.omgwait { color: var(--muted); font-size: .8em; margin-top: 10px;
           border-top: 1px solid var(--border); padding-top: 8px;
           display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.spin { width: 11px; height: 11px; flex: none; border-radius: 50%;
        border: 2px solid var(--border); border-top-color: var(--muted);
        animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }

/* What the press wrote. The least saturated rail on the panel: the only block whose content
   did not come from this receiver. */
:root { --nieuws: #7c8aa0; }
[data-theme="dark"] { --nieuws: #8b9ab0; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --nieuws: #8b9ab0; } }

.omgeving.nieuws { border-left-color: var(--nieuws); }
.omgeving.nieuws .omgtitle { color: var(--nieuws); }
.omgeving.nieuws .omgtitle ico { vertical-align: -0.18em; margin-right: .3em; }

/* A row is the headline plus who wrote it. The headline wraps — a news title is a sentence,
   not a label, and truncating it would hide the half that says which street. */
.capline.newsline {
  display: block; text-decoration: none; color: inherit;
  padding: 7px 8px; border-radius: 6px;
}
.capline.newsline:hover { background: var(--panel2); }
.newshead { display: block; font-weight: 600; line-height: 1.35; }
.newsout { color: var(--nieuws); font-weight: 400; }
.newssrc { display: block; margin-top: 2px; font-size: .85em; opacity: .7; }

/* The hectometerpaaltje drawn as the board it is: RAL 6024 verkeersgroen with the reflective
   white border. Only for `hectometre` precision, the one answer where this app knows the
   exact object it points at. See docs/decisions/frontend.md#the-map. */
.hmboard {
  width: 58px; height: 40px; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  background: #00694e; border: 2px solid #fff; border-radius: 3px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .55);
  font-family: var(--font-ui); line-height: 1;
}
.hmb-top { display: flex; align-items: center; gap: 3px; }
/* The number keeps .rw-a/.rw-n, so the map cannot disagree with the row about the colour a
   road wears. Only the size is board-specific. */
.hmboard .rw { font-size: 10px; padding: 0 3px 1px; border-width: 1px; }
.hmb-side { color: #fff; font-size: 10px; font-weight: 600; }
.hmb-km { color: #fff; font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* The day a row belongs to, drawn only when it is not today — scroll back past midnight and
   23:41 reads as tonight. Under the clock rather than beside it: the time column is sized for
   HH:MM:SS and a date on the same line would widen every row or wrap. */
/* A margin rather than a space character: the template's whitespace sits against a closing
   tag, which HTML is free to collapse — and did. */
.c-day, .cardday { font-size: .78em; opacity: .75; white-space: nowrap; margin-left: .35em; }

/* ── Aanrijtijden ────────────────────────────────────────────────────────────────────────
   The ninth panel and the ninth hue, the rule `--nieuws` already states: one block, one
   colour, said once. Indigo because the eight before it took the blues, the greens and the
   purples — and deliberately not a discipline colour, which each *row* wears instead: the
   bar in a row is the same claim the map's route line makes, so it must be the same hue as
   that line or the panel and the map would be colouring one fact two ways. */
:root { --uitruk: #7a86f0; }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) { --uitruk: #4338ca; } }
[data-theme="light"] { --uitruk: #4338ca; }

.omgeving.uitruk { border-left-color: var(--uitruk); margin: 12px 0; }
.omgeving.uitruk > .omgtitle { color: var(--uitruk);
                               display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* "schatting" beside the heading rather than in a tooltip: it is a claim about how good
   every number under it is, and the reader who would not open a tooltip is the one it is
   for. Muted and outlined, so it reads as a caveat and not as a priority badge. */
.u-est { font-size: .82em; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
       color: var(--muted); border: 1px solid var(--border); border-radius: 3px;
       padding: 0 5px 1px; }
.btn.tiny.replay { margin-left: auto; display: inline-flex; align-items: center; gap: 4px;
                   text-transform: none; letter-spacing: 0; }
.btn.tiny.replay ico { font-size: .95em; }
.btn.tiny.replay.on { border-color: var(--uitruk); color: var(--uitruk); }

.uitruklist { margin-top: 8px; }
.uitrukrow { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px;
             align-items: baseline; padding: 9px 0; }
.uitrukrow + .uitrukrow { border-top: 1px solid var(--border); }
/* One custom property per row, so everything below reads `--u` and nothing restates which
   discipline is which. `other` keeps the panel's own hue rather than falling through to a
   colour that would claim a service. */
.uitrukrow { --u: var(--uitruk); }
.uitrukrow.svc-fire { --u: var(--fire); }
.uitrukrow.svc-police { --u: var(--police); }
.uitrukrow.svc-ambulance { --u: var(--ambulance); }

.u-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
.u-who { font-weight: 600; overflow-wrap: anywhere; }
.u-pill { font-size: .74em; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
          padding: 1px 7px 2px; border-radius: 999px; white-space: nowrap; }
.u-pill.p-wait { background: var(--border); color: var(--muted); }
.u-pill.p-drive { background: var(--u); color: var(--bg); }
.u-pill.p-there { border: 1px solid var(--border); color: var(--muted); }

/* The clock is the biggest thing in the row because it is what somebody opened this to read,
   with what kind of number it is directly under it — a clock looks exactly as certain as a
   measurement, so the word has to travel with it. */
.u-eta { grid-row: span 2; align-self: center; text-align: right;
         font-variant-numeric: tabular-nums; white-space: nowrap; }
.u-eta b { display: block; font-size: 1.45em; font-weight: 600; letter-spacing: -.02em; }
.u-eta small { display: block; font-size: .7em; font-weight: 500; letter-spacing: .06em;
               text-transform: uppercase; color: var(--muted); }
.u-sub { font-size: .86em; color: var(--muted); font-variant-numeric: tabular-nums; }

.u-bar { grid-column: 1 / -1; position: relative; height: 8px; border-radius: 4px;
         background: var(--border); overflow: hidden; margin-top: 7px; }
.u-bar i { position: absolute; top: 0; bottom: 0; display: block; }
.u-bar.waiting { background: var(--border); opacity: .5; }

/* Arcering, doing two jobs at once. It separates the two phases without a second hue — so
   the split survives greyscale and colour blindness, where two tints of one colour do not —
   and the hatched half is also the *rougher* of the two claims: a rijtijd comes from a
   routing engine over real road geometry, an uitruktijd is a figure somebody typed. Texture
   for "we are least sure here" is a convention a drawing already has. */
.u-z1, .u-z2 { background: color-mix(in srgb, var(--u) 14%, transparent); left: 0; right: 0; }
.u-f1 { background: repeating-linear-gradient(135deg, var(--u) 0 3px, transparent 3px 7px);
        left: 0; }
.u-f2 { background: var(--u); }
/* The seam, so the split is legible even where the fill has not reached it yet. */
.u-bar .u-seam { width: 1px; background: var(--panel2); z-index: 2; opacity: .9; }

.u-legend { display: flex; gap: 4px 14px; flex-wrap: wrap; margin-top: 10px;
            font-size: .82em; color: var(--muted); align-items: center; }
.u-legend span { display: inline-flex; align-items: center; gap: 6px; }
.u-legend .muted { color: var(--muted); opacity: .8; }
/* The swatches are the bar's own classes, so a legend cannot come to disagree with what it
   names. They need a box of their own only because they are not inside a .u-bar. */
.u-sw { position: static; display: block; width: 16px; height: 8px; border-radius: 3px;
        flex: none; }
/* The hatch is a background-*image*, so `.u-f1`'s shorthand cleared the tint underneath it;
   this puts that swatch back to what a filled bar actually looks like. **Only the hatched
   one** — written for `.u-sw` at large it lands on the solid swatch too, which comes earlier
   in this file and so loses its colour to a 14% wash: a legend naming "rijden" in grey. */
.u-sw.u-f1 { background-color: color-mix(in srgb, var(--u) 14%, transparent); }
/* `--u` comes off the legend itself, which carries the discipline every row shares — and the
   panel's own hue where they do not agree. Written here and not on `.u-sw`, so a swatch can
   never define a colour of its own and drift from the bars it names. */
.u-legend { --u: var(--uitruk); }
.u-legend.svc-fire { --u: var(--fire); }
.u-legend.svc-police { --u: var(--police); }
.u-legend.svc-ambulance { --u: var(--ambulance); }

.u-why { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--border);
         font-size: .86em; color: var(--muted); }
.u-why p { margin: 0; }
.u-why b { color: var(--text); font-weight: 600; }
.u-why details { margin-top: 6px; }
.u-why summary { cursor: pointer; color: var(--muted); opacity: .85; }
.u-why details p { margin-top: 7px; }

/* On a phone the clock keeps its size and moves above the facts rather than squeezing the
   post's name into two words. */
@media (max-width: 560px) {
  .uitrukrow { grid-template-columns: 1fr auto; }
  .u-eta b { font-size: 1.25em; }
}
