/* Everything colour lives in these tokens, and nothing below this block names
   a colour directly.  That is what makes the light theme a second copy of the
   token list rather than a second copy of the stylesheet.

   Three states, because "system" is a real setting and not the absence of one:
   bare :root is light, the prefers-color-scheme block is the system default,
   and the [data-theme] blocks are an explicit choice that has to win over the
   media query in both directions. */
:root {
  --bg: #f3f4f7;
  --panel: #ffffff;
  --panel-2: #eceef3;
  /* One step further along the surface ramp than --panel-2, which means
     darker in light and lighter in dark.  A hover painted with it reads as
     a change in both themes rather than in only one. */
  --panel-3: #d8dce7;
  --panel-glass: #ffffffee;
  --line: #d5d9e2;
  --line-soft: #e5e8ee;
  --ink: #171a21;
  --dim: #5b6373;
  --accent: #d23726;
  --accent-ink: #ffffff;
  --accent-2: #9a5a00;
  /* The ink that goes on --accent-2, which is not --accent-ink even where the
     two are the same colour.  --accent-ink is what a primary button and the
     queue's call button paint on --accent and on --ok, so an ink dark enough
     to be read on the dark theme's orange cannot be spelled by changing that
     one: it would put dark brown on the dark theme's red.  White measures
     5.47:1 on this brown, which is why the light half of this was never
     wrong. */
  --accent-2-ink: #ffffff;
  --ok: #10794f;
  --open: #d23726;
  --reported: #b97600;
  --resolved: #10794f;
  --dismissed: #767d8d;
  --map-bg: #e6e9ef;
  --backdrop: #1119;
  --error-bg: #fbe4e1;
  --shadow: 0 6px 24px #0f172a1f;

  /* Map ink: the marker outline and the watch casing, which have to
     stay legible against the tiles rather than against the page. */
  --map-stroke: #ffffff;
  --map-casing: #33384a;
  --hotspot: #d23726;
  /* A repeat is one car coming back, which is a different fact from a bad
     corner, so it is a different colour rather than a second red ring. */
  --repeat: #7b3fb5;
  --watch: #0f9c85;
  /* A zone is a boundary somebody drew rather than something that happened,
     so it is the one colour on the map that is not a status or a count. */
  --zone: #3a63c8;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161a;
    --panel: #1c1f26;
    --panel-2: #232733;
    --panel-3: #2d3242;
    --panel-glass: #1c1f26f2;
    --line: #2f3542;
    --line-soft: #23272f;
    --ink: #e8eaf0;
    --dim: #9aa3b2;
    --accent: #ff5a4d;
    --accent-ink: #ffffff;
    --accent-2: #ffb02e;
    /* Dark on the orange rather than white: white measures 1.83:1 here, and
       the sentence painted with it is the preview's, the one saying this is
       not the real site and that everything on it goes overnight.  #221a00 on
       it measures 9.46:1. */
    --accent-2-ink: #221a00;
    --ok: #3ecf8e;
    --open: #ff5a4d;
    --reported: #ffb02e;
    --resolved: #3ecf8e;
    --dismissed: #7a8296;
    --map-bg: #0f1115;
    --backdrop: #000a;
    --error-bg: #46212099;
    --shadow: 0 6px 24px #0008;

    --map-stroke: #12141a;
    --map-casing: #0a0d12;
    --hotspot: #ff5a4d;
    --repeat: #c08cff;
    --watch: #2ff0c2;
    --zone: #6f9dff;
  }
}

:root[data-theme="dark"] {
  --bg: #14161a;
  --panel: #1c1f26;
  --panel-2: #232733;
  --panel-3: #2d3242;
  --panel-glass: #1c1f26f2;
  --line: #2f3542;
  --line-soft: #23272f;
  --ink: #e8eaf0;
  --dim: #9aa3b2;
  --accent: #ff5a4d;
  --accent-ink: #ffffff;
  --accent-2: #ffb02e;
  --accent-2-ink: #221a00;
  --ok: #3ecf8e;
  --open: #ff5a4d;
  --reported: #ffb02e;
  --resolved: #3ecf8e;
  --dismissed: #7a8296;
  --map-bg: #0f1115;
  --backdrop: #000a;
  --error-bg: #46212099;
  --shadow: 0 6px 24px #0008;

  --map-stroke: #12141a;
  --map-casing: #0a0d12;
  --hotspot: #ff5a4d;
  --repeat: #c08cff;
  --watch: #2ff0c2;
  --zone: #6f9dff;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* Every page is the same column: the topbar, the read-only bar when a
   maintenance window is running, the page, and the footer.  main.full -- the
   map, and only the map -- takes whatever is left over, so its height is
   measured off the bars above it rather than assumed.

   One layout for both states on purpose.  A second one that only assembles
   itself while a window is running is a layout nobody looks at until the one
   hour a year somebody does, and the height it has to get right is the same
   height in both cases.

   The `height: 100%` above is what makes `flex: 1` mean the space left over:
   100% of an html that is itself 100% is a definite height, and a flex
   container sized by content with a `min-height` floor is the case engines
   have historically disagreed about.  A page taller than the viewport
   overflows the body box and scrolls, exactly as it did when this was block
   layout -- nothing here shrinks below its content. */
body { display: flex; flex-direction: column; }

/* The picker is server-rendered and the theme applied before paint, so this
   only ever fires on a live change from the settings page. */
html.theme-switching, html.theme-switching * { transition: background-color .18s, color .18s, border-color .18s; }

a { color: var(--accent-2); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------------------------------------------------------------- topbar */
.topbar {
  display: flex; align-items: center; gap: 20px;
  padding: 10px 16px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 1000;
}
.brand { font-weight: 800; letter-spacing: -0.5px; color: var(--ink); font-size: 18px; }
.brand span { color: var(--accent); }
.topbar nav { display: flex; gap: 14px; flex: 1; flex-wrap: wrap; }
.topbar nav a { color: var(--dim); font-weight: 500; }
.topbar nav a.on, .topbar nav a:hover { color: var(--ink); text-decoration: none; }
.topbar .who { display: flex; align-items: center; gap: 10px; color: var(--dim); }
.topbar .who .name { color: var(--ink); }
.badge {
  display: inline-block; min-width: 18px; padding: 0 5px; margin-left: 5px;
  background: var(--accent); color: var(--accent-ink); border-radius: 9px;
  font-size: 11px; text-align: center; line-height: 18px;
}
/* The bell, which is where the notifications entry went.  It is an anchor, so
   it inherits the link colour and has to be told not to underline on hover the
   way the bar's entries are; the badge rides inside it rather than beside it,
   because the count belongs to the bell and a gap between them is a place for
   the two to wrap apart on a narrow screen. */
.bell { position: relative; color: var(--dim); font-size: 17px; line-height: 1; }
.bell:hover, .bell.on { color: var(--ink); text-decoration: none; }
.bell .badge { margin-left: 2px; vertical-align: 2px; }

/* Anchored to the topbar rather than to the bell, which is an inline anchor
   and a poor thing to position a card against: the header is already
   `position: relative` and carries the z-index the map's own panes are kept
   under, so the panel hangs off its bottom edge and stays above everything a
   page draws.  Right-aligned because the bell is at the right-hand end, and
   capped in height because "the most recent few" is five rows today and
   nothing stops the answer growing. */
.notif-panel {
  position: absolute; top: 100%; right: 12px; z-index: 1;
  width: min(340px, calc(100vw - 24px)); max-height: 60vh; overflow-y: auto;
  padding: 10px 14px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .25);
}
.notif-panel[hidden] { display: none; }
.notif-head, .notif-foot { display: flex; justify-content: space-between; align-items: center; }
.notif-head { padding-bottom: 6px; border-bottom: 1px solid var(--line); }
.notif-foot { padding-top: 8px; }
.notif-actions { margin: 0 0 10px; }

.role-tag {
  display: inline-block; padding: 0 6px; margin-left: 6px;
  border: 1px solid var(--line); border-radius: 999px;
  font-size: 10px; text-transform: uppercase; letter-spacing: .5px; color: var(--dim);
}

/* --------------------------------------------------------- read-only bar */
/* Only ever on the page while a maintenance window is running, and it needs no
   layout rules of its own: it is a row of the column every page already is, so
   whether it comes out one line deep on a desktop or three on a phone is a
   height that has been measured rather than guessed at, and the map below it
   is short by exactly that much. */
.readonly-bar {
  padding: 8px 16px; text-align: center; font-size: 13px;
  background: var(--error-bg); color: var(--ink);
  border-bottom: 1px solid var(--line);
}

/* --------------------------------------------------------- the preview bar */
/* The same row, on the deployment that is not the real site.  It borrows the
   read-only bar's shape deliberately -- both are a sentence across the top of
   every page and both want the map under them to be short by exactly what
   they are tall -- and differs in colour because they say different things
   and can be on the page at once: the preview's nightly window is a window
   like any other, so at 02:00 there are two of these stacked. */
.preview-bar {
  padding: 8px 16px; text-align: center; font-size: 13px;
  background: var(--accent-2); color: var(--accent-2-ink);
  border-bottom: 1px solid var(--line);
}

/* What the window is going to refuse, greyed before it is pressed rather than
   explained after it.  assets/js/readonly.js marks every control that writes,
   and this is the look `.btn:disabled` already has: a control that will do
   nothing should look the way every other dead control on the site looks.

   `data-refused` is that file's mark and the whole of the selector, so the
   greying and the swallowed click cannot come apart -- one pass sets both,
   and neither is a list of controls written out a second time here.  It is
   what is styled rather than the aria-disabled beside it because that one is
   not ours alone: Leaflet marks the zoom buttons with it at each end of the
   zoom range, and a zoom is not a write.

   Whose buttons grey is not asked here.  There is no `data-role` in this
   selector on purpose: an account the gate exempts entirely has nothing
   marked to grey, because the layout tells readonly.js so and it stops -- so
   the condition is the plain `body.read-only`, and it stays that way when the
   super-admin exemption narrows and there is no exempt account left. */
body.read-only [data-refused],
body.read-only [data-refused]:hover {
  opacity: .55; cursor: default; filter: none;
}

/* ----------------------------------------------------------------- main */
main { padding: 20px; max-width: 1100px; margin: 0 auto; }
/* The map fills whatever the topbar and any bar under it left behind.
   `min-height: 0` says that and nothing else decides its height: a flex item
   is otherwise refused a size below its own contents, and a map is a thing you
   scroll rather than a thing that grows.  `position: relative` is what the two
   children position against -- both are absolutely positioned, `.map` on
   `inset: 0` and `.panel` off three edges, so without it they measure
   themselves against the viewport and the panel comes away from the map. */
main.full { padding: 0; max-width: none; flex: 1; min-height: 0; position: relative; }
main.wide { max-width: 1400px; }
/* A flex item with two auto margins is sized to its content rather than
   stretched, so main and the footer would go as narrow as whatever is inside
   them without this.  With border-box everywhere, 100% and a max-width is
   exactly what they do outside a flex container. */
body > main, body > .sitefoot { width: 100%; }

/* Mirrors main's own widths so the rule above it lines up with the content
   on the wide pages as well as the ordinary ones. */
.sitefoot { max-width: 1100px; margin: 0 auto; padding: 0 20px 24px; }
.sitefoot.wide { max-width: 1400px; }
.sitefoot p {
  margin: 0; padding-top: 14px; border-top: 1px solid var(--line-soft);
  color: var(--dim); font-size: 13px;
}

.card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 18px 20px; margin-bottom: 16px;
}
.card.narrow { max-width: 420px; margin: 40px auto; }
h1 { font-size: 22px; margin: 0 0 8px; }
h2 { font-size: 17px; margin: 0 0 10px; }
h3 { font-size: 14px; margin: 0 0 6px; }
.hint, .dim { color: var(--dim); }
.small { font-size: 12px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.big { font-size: 17px; letter-spacing: 1px; }
.empty { color: var(--dim); font-style: italic; }
.error { background: var(--error-bg); border: 1px solid var(--accent); padding: 8px 10px; border-radius: 8px; }
.notice { background: var(--panel-2); border: 1px solid var(--line); padding: 8px 10px; border-radius: 8px; }
.ok { color: var(--ok); }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.ellipsis { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --------------------------------------------------------------- forms */
label { display: block; margin: 10px 0; color: var(--dim); font-size: 13px; }
input, select, textarea {
  display: block; width: 100%; margin-top: 4px; padding: 8px 10px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink); font: inherit;
}
label.check { display: flex; align-items: center; gap: 8px; }
label.check input { width: auto; margin: 0; }
/* The violation, which is a radio pair and eight independent flags.  Laid out
   as a wrapping row rather than a column because eight checkboxes stacked are
   a form somebody scrolls past: they are the fast half of filing a report, and
   they have to look like a row of ticks and not like a questionnaire. */
fieldset.violation {
  margin: 10px 0 0; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 8px;
}
fieldset.violation legend { padding: 0 4px; color: var(--dim); font-size: 13px; }
fieldset.violation label.check { display: inline-flex; margin: 4px 12px 4px 0; }
form.inline { display: inline; }
.row-form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.row-form label { margin: 0; }

.btn {
  display: inline-block; padding: 9px 14px; border: 0; border-radius: 8px;
  background: var(--accent); color: var(--accent-ink); font: inherit; font-weight: 600; cursor: pointer;
}
.btn:hover { filter: brightness(1.1); text-decoration: none; }
.btn:disabled { opacity: .55; cursor: default; filter: none; }
.btn.ghost { background: transparent; border: 1px solid var(--line); color: var(--ink); }
.btn.small { padding: 6px 10px; font-size: 13px; }
.btn.tiny { padding: 3px 8px; font-size: 12px; }
.btn.danger { background: var(--open); }
button.link { background: none; border: 0; color: var(--dim); cursor: pointer; font: inherit; padding: 0; }
button.link:hover { color: var(--ink); }

/* ---------------------------------------------------------------- stats */
.stats { display: flex; gap: 18px; flex-wrap: wrap; }
.stats div { background: var(--panel-2); border-radius: 10px; padding: 10px 14px; min-width: 92px; }
.stats strong { display: block; font-size: 20px; }
.stats span { color: var(--dim); font-size: 12px; }
/* The line saying how old the three numbers are, inside the section rather
   than under it: it is a sentence about these counters and nothing else, so
   it belongs to them when the detail panel puts the whole section away.  A
   full basis is what gives it its own row under the boxes instead of a fourth
   box beside them. */
.stats .counted { flex-basis: 100%; margin: 0; line-height: 1.4; }

/* ---------------------------------------------------------------- pills */
.pill {
  display: inline-block; padding: 1px 8px; border-radius: 999px;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px;
  background: var(--dismissed); color: var(--panel);
}
.pill.open { background: var(--open); color: #fff; }
.pill.reported { background: var(--reported); color: #241a00; }
.pill.resolved { background: var(--resolved); color: #04281a; }

/* --------------------------------------------------------------- tables */
table.board { width: 100%; border-collapse: collapse; margin-top: 8px; }
table.board th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .6px;
  color: var(--dim); border-bottom: 1px solid var(--line); padding: 6px 8px;
}
table.board td { padding: 7px 8px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
table.board td.num, table.board td.rank { text-align: right; font-variant-numeric: tabular-nums; }
table.board td.rank { color: var(--dim); width: 30px; }
table.board tr.this-one td { background: var(--panel-2); }

ul.rows { list-style: none; padding: 0; margin: 0; }
ul.rows li { padding: 8px 0; border-bottom: 1px solid var(--line-soft); }
ul.notif li.unread { border-left: 3px solid var(--accent); padding-left: 10px; }

.tabs { display: flex; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
.tabs a { color: var(--dim); padding-bottom: 4px; }
.tabs a.on { color: var(--ink); border-bottom: 2px solid var(--accent); }

/* The second row of tabs on the queue: which town, under which status.  A
   smaller type size so the two rows read as a heading and a subheading of one
   another rather than as two controls of equal weight, and the count against
   each choice dimmer still -- it is there to be glanced at, and a town with
   nothing in it should not shout the loudest. */
.tabs.bins { font-size: 13px; gap: 10px; margin-top: 6px; }
.tabs.bins .bin-count { color: var(--dim); font-variant-numeric: tabular-nums; }
.tabs.bins a.on .bin-count { color: var(--ink); }
/* The towns the list did not have room for.  Dimmer than a town and italic
   because it is the one thing in this row that cannot be clicked -- there is
   no page for the ones that did not fit, and it should not look like a link
   that is broken. */
.tabs.bins .bin-more { color: var(--dim); font-style: italic; }

/* ------------------------------------------------------------------ map */
.map { position: absolute; inset: 0; background: var(--map-bg); }
.leaflet-container { background: var(--map-bg); font: inherit; }

.panel {
  position: absolute; top: 12px; right: 12px; bottom: 12px; width: 340px;
  background: var(--panel-glass); backdrop-filter: blur(6px);
  border: 1px solid var(--line); border-radius: 12px;
  z-index: 900; display: flex; overflow: hidden; box-shadow: var(--shadow);
}
.panel.collapsed { width: 26px; }
.panel.collapsed .panel-inner { display: none; }
.panel-inner { padding: 14px; overflow-y: auto; flex: 1; }
.panel-toggle {
  width: 26px; border: 0; border-right: 1px solid var(--line);
  background: var(--panel-2); color: var(--dim); cursor: pointer;
}
.panel section { margin-bottom: 16px; }
.panel .filters { display: flex; gap: 10px; flex-wrap: wrap; }
.panel .filters label { flex: 1 1 130px; margin: 0; }
/* A note about a checkbox belongs under the row of them rather than beside
   one, so it takes the whole line rather than a column of its own. */
.panel .filters .hint { flex: 1 1 100%; margin: 0; font-size: 12px; }
.panel .actions { display: flex; gap: 8px; flex-wrap: wrap; }
.panel .list h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .6px; color: var(--dim); }
.panel .list ul { list-style: none; padding: 0; margin: 0; }
.panel .list li {
  padding: 7px 0; border-bottom: 1px solid var(--line-soft); font-size: 13px; cursor: pointer;
}
.panel .list li:hover { color: var(--ink); }
.panel .list li .dim { font-size: 12px; }

/* The zone outlines.  Painted through `color` and a class, the way the
   editor paints its own, so the theme is answered by the stylesheet rather
   than re-read and re-applied by the script every time it changes. */
.leaflet-container .map-zone { color: var(--zone); }

/* The name sits on the zone as a small plate, because a zone is drawn over
   map tiles and bare text on those is only legible over some of them.  The
   marker itself is zero-sized and carries Leaflet's positioning transform,
   so the centring is done here, on the child. */
.zone-label span {
  position: absolute;
  transform: translate(-50%, -50%);
  white-space: nowrap;
  padding: 1px 8px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel-glass);
  color: var(--ink);
  font-size: 12px;
  font-weight: 600;
  box-shadow: var(--shadow);
  cursor: pointer;
}
.zone-label span:hover { background: var(--panel-3); }
/* While a click is going to place a report or a watch, the names step
   out of its way: the crosshair says the next click lands on the map, and a
   name that filled the panel instead would be making a liar of it. */
.map.placing .zone-label span { pointer-events: none; }

/* A watch whose ring is one or two points -- Geo.php's own name for it is a
   "degenerate ring" -- has no area for watchCircles()'s polygon to fill, so
   it is drawn as a small dot rather than as the bounding circle
   Region::toWatch() sends for it.  Centred the same way a zone's name is:
   the marker itself is zero-sized, so the dot is the child that carries the
   centring. */
.watch-point span {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--watch);
  border: 2px solid var(--map-casing);
  cursor: pointer;
}
.map.placing .watch-point span { pointer-events: none; }

/* -------------------------------------------------- vendored Leaflet chrome

   Leaflet's stylesheet paints its own controls -- `.leaflet-bar a` is
   #fff on black, hard-coded -- so on every page with a map the zoom buttons
   ignored the theme entirely and sat there as a white box in the dark.

   The overrides live here rather than in map.css because three different
   pages put a zoom control on screen (the map, the batch reporter and the
   zone editor) and they load three different sheets; layout.php loads
   leaflet.css and then this file on all of them.  There is no popup chrome to
   theme any more: what a click on this map says now goes in the side panel,
   which is the site's own furniture and takes the site's own colours. */
.leaflet-bar { box-shadow: var(--shadow); }
.leaflet-bar a {
  background: var(--panel);
  color: var(--ink);
  border-bottom-color: var(--line);
}
.leaflet-bar a:hover, .leaflet-bar a:focus { background: var(--panel-3); color: var(--ink); }
/* Still a button, still readable, and still obviously not one you can press:
   the end of the zoom range is a state, not a disappearance. */
.leaflet-bar a.leaflet-disabled { background: var(--panel-2); color: var(--dim); }
.leaflet-touch .leaflet-bar { border-color: var(--line); }

dialog {
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: 12px; padding: 20px; width: min(420px, 92vw);
}
dialog::backdrop { background: var(--backdrop); }
dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; margin: 16px 0 0; }
dialog .where { color: var(--dim); font-size: 13px; font-family: ui-monospace, Menlo, monospace; }

/* -------------------------------------------------------------- report */
.report-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
/* The button on the end of that row does not give.  Flex items shrink under
   pressure by default, and what shares the row with this one is a heading and
   its paragraph -- the zone editor's four lines of drawing instructions are
   the longest of them -- so the row asks for far more width than the card has
   and takes the difference out of both items in proportion to what they asked
   for.  The paragraph has a break at every space; the button has one, in the
   middle of "Back to zones", and it was taking it at any ordinary window
   width.  `report.php` puts a `.report-actions` column in that slot instead of
   a button, and that one is meant to be able to give. */
.report-head > .btn { flex-shrink: 0; }
.facts { display: grid; grid-template-columns: 110px 1fr; gap: 6px 14px; margin: 16px 0; }
.facts dt { color: var(--dim); font-size: 13px; }
.facts dd { margin: 0; }
.media-grid { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0; }
.media-grid img, .media-grid video { max-width: 100%; max-height: 240px; border-radius: 10px; border: 1px solid var(--line); }
/* Wide screens put the photograph to the left of the facts rather than under
   them: the picture is what the page is about and the facts are what is
   claimed about it, and stacked they push the evidence below the fold while a
   third of the window sits empty beside a column of short text.

   Scoped to a card that actually has one, because `report.php` leaves the
   `.media-grid` out entirely when there is no media -- the selector then stops
   matching, the card falls back to ordinary block flow, and there is no empty
   column left behind.  A browser without `:has()` lands in that same fallback,
   which is exactly today's page. */
@media (min-width: 721px) {
  .card:has(> .media-grid) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* `dense` is load-bearing.  The markup runs facts-then-media but the
       columns run media-then-facts, so by the time `.media-grid` asks for
       column 1 the sparse cursor has already passed it and would drop the
       photograph onto a row of its own -- back to stacked.  `order:` is not
       the alternative: it reorders auto-placement for every child and drags
       the media above the heading. */
    grid-auto-flow: row dense;
    align-items: start;
    column-gap: 20px;
  }
  /* Everything else on the card -- the heading, the clip links, the map, the
     owner's form -- keeps the full width it had.  Anything added to the card
     later inherits this and needs its own `grid-column` to share a row. */
  .card:has(> .media-grid) > * { grid-column: 1 / -1; }
  .card:has(> .media-grid) > .facts { grid-column: 2; }
  /* Margins stop collapsing inside a grid, so the pair's tops only line up if
     they are told to: `.facts` carries 16px and `.media-grid` 12px. */
  .card:has(> .media-grid) > .media-grid { grid-column: 1; margin-top: 16px; }
  /* A lone photograph fills its column instead of the 240px cap it wore when
     it was a thumbnail under the text.  Several still wrap down the column at
     240px each, which is what a column of evidence should look like. */
  .media-grid:has(> :only-child) img, .media-grid:has(> :only-child) video {
    max-height: min(60vh, 480px);
  }
}

/* The marks on a clip.  The track is the whole film end to end and the span
   over it is the part somebody kept, so a report whose clip has been marked
   says so at a glance rather than only inside two number fields.  Full width
   of the form so it lines up over the controls that set it, and the fields
   below it: the flex row would otherwise put the track between them at
   whatever width was left over. */
.clip-marks { align-items: flex-end; margin-top: 12px; }
.clip-marks .clip-name { flex: 1 0 100%; margin: 0; }
.clip-marks .clip-track {
  flex: 1 0 100%; position: relative; height: 8px; border-radius: 4px;
  background: var(--line-soft); border: 1px solid var(--line); cursor: pointer;
}
.clip-marks .clip-span {
  position: absolute; top: 0; bottom: 0; border-radius: 4px; background: var(--accent);
}
/* A hairline rather than a handle: it says where the film is, and the scrubber
   on the video itself is what moves it. */
.clip-marks .clip-head {
  position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--ink);
}
.clip-marks input[type="number"] { width: 9ch; }

.mini-map { height: 240px; border-radius: 10px; margin-top: 12px; }

.queue-item { display: flex; gap: 16px; align-items: flex-start; }
.queue-item .thumb img, .queue-item .thumb video {
  width: 160px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid var(--line);
}
.queue-body { flex: 1; }
.queue-body h2 { margin: 0 0 4px; }
.queue-body .addr { margin: 0 0 4px; }
.queue-body .meta { color: var(--dim); font-size: 13px; margin: 0 0 10px; }
.queue-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* Pushed to the far end: it is the one button in the row that cannot be
   taken back, and it should not sit where the thumb lands by accident. */
.queue-actions .queue-delete { margin-left: auto; }

/* The call is what the queue is for, so it is the green one, the same as the
   Call button on the report page's contact panel. */
/* The name a report or a group is known by.  It is meant to be read out, so
   it gets the letter-spacing that stops two syllables running together. */
.ident { letter-spacing: .04em; }

.queue-contact { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 0 0 10px; font-size: 13px; }
.queue-contact .queue-call { background: var(--ok); color: var(--accent-ink); }

/* The disclosure that used to be its own tab: closed, it is one line that
   says how many hot spots are folded away in it; open, it draws the same
   cards that tab used to.  The marker is the browser's own triangle, which
   is the down-arrow the summary line asks for without any JS of ours. */
.hotspot-fold summary {
  cursor: pointer; font-weight: 600; padding: 12px 16px; margin-bottom: 16px;
  border-radius: 12px; background: var(--panel); border: 1px solid var(--line);
}
.hotspot-fold[open] summary { margin-bottom: 12px; }
.hotspot-fold .hint { margin: 0 0 16px; }

/* The pop-over.  Wider than the form dialogs because the photo is the point
   of it, and scrollable because a report with three of them is taller than
   the screen. */
dialog.shot-dialog { width: min(760px, 94vw); max-height: 92vh; overflow-y: auto; }
.shot-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.shot-head h2 { margin: 0 0 8px; }
.shot-media { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.shot-media img, .shot-media video {
  width: 100%; max-height: 60vh; object-fit: contain;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
}
.shot-dialog .addr { margin: 0 0 4px; }
.shot-dialog .meta { margin: 0 0 10px; font-size: 13px; }
.shot-dialog .mini-map { height: 200px; }
.shot-dialog menu { flex-wrap: wrap; }

/* The wider copy of the same pop-over, used by the batch page and by a report's
   evidence: nothing shares the room with the photograph in either, and looking
   at it closely -- is that plate legible, is that wheel really on the kerb --
   is the whole reason it opens. */
dialog.shot-full { width: min(1100px, 96vw); }
/* The film as well as the photograph: a clip opened to be cropped is opened
   for the same reason a photograph is, and the rectangle is dragged over
   whichever of the two is in the panel. */
.shot-full .shot-media img, .shot-full .shot-media video { max-height: 74vh; }

/* Cropping the photograph a report's viewer has open.  The frame is there to
   be the thing the rectangle is positioned against, and it takes the picture's
   own height rather than the column's: `line-height: 0` is what stops the
   inline box under an <img> adding a few pixels of nothing to it, which the
   shade below would otherwise darken as though it were part of the shot. */
.shot-crop { position: relative; overflow: hidden; line-height: 0; }
.shot-crop-box {
  position: absolute; border: 1px dashed var(--accent); border-radius: 2px;
  /* Everything outside the rectangle, dimmed in one declaration rather than in
     four boxes drawn round it.  The spread is far wider than any dialog, and
     the frame clipping its overflow is what keeps it off the rest of the
     page. */
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}
/* A drag across a picture is a scroll on a touch screen unless the element
   says otherwise, and here the drag is the feature.  The film gets neither the
   crosshair over its whole box nor `touch-action: none`: the bottom of it is
   the player's own controls, and a scrub somebody cannot drag is a film they
   cannot find the moment in. */
.shot-crop img { cursor: crosshair; touch-action: none; }
.shot-crop video { cursor: crosshair; }

/* Zooming the picture a pop-out viewer has opened, which `zoom.js` does the
   same way in all four of them: this box round the picture is what a magnified
   photograph is clipped to, and the picture inside it is transformed.  Without
   the clip a photograph zoomed three times would be three panels wide and
   painted across the rest of the page.

   `touch-action` for the reason the crop's own rule above gives it, and for
   one more: two fingers over a picture are a pinch here, and a browser left to
   its own devices answers them by zooming the whole page. */
.shot-zoom { position: relative; overflow: hidden; line-height: 0; touch-action: none; }
/* Beaten by neither of the two rules that also name a picture in this panel:
   the crop's crosshair says a drag draws a rectangle, and that is exactly what
   a drag stops doing once there is somewhere to pan to. */
.shot-zoom.zoomed img, .shot-zoom.zoomed video { cursor: grab; }
/* In the corner of the picture rather than under it.  The picture is as tall
   as the window will take, so a control below it is a control off the bottom
   of the panel exactly when somebody is looking for it. */
.shot-zoom-fit {
  position: absolute; right: 8px; top: 8px; z-index: 2; line-height: normal;
}
/* And the control that says the next drag draws a rectangle rather than
   panning, in the other corner of the same picture.  Opposite rather than
   beside it because the two are shown together -- both appear the moment the
   picture is magnified -- and a corner apiece is what keeps either from
   depending on how wide the other's label happens to be. */
.shot-zoom-draw {
  position: absolute; left: 8px; top: 8px; z-index: 2; line-height: normal;
}
/* Beating the grab hand the rule above puts on a magnified picture, for the
   reason that rule beats the crosshair: whichever of the two a drag is about
   to do is the one the pointer has to say. */
.shot-zoom.drawing img, .shot-zoom.drawing video { cursor: crosshair; }
.shot-crop-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.shot-crop-bar .hint { margin: 0; flex: 1 1 auto; }
.shot-full .facts { margin: 0; }
.shot-full .shot-place { margin: 14px 0 0; }
/* A registration is seven characters wide, and the form rule up the file makes
   every input as wide as its box -- which in a 1100px dialog is a number plate
   a metre long. */
.shot-full .shot-plate { margin: 14px 0 0; }
/* The plate is the one field this dialog exists to fill in, so it gets to be
   the biggest text in it -- large and spaced out like a real plate, not the
   same size as the labels around it. */
.shot-full .shot-plate input {
  max-width: 220px; font-size: 22px; letter-spacing: 2px; padding: 6px 8px;
}

pre.api, pre.token {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 12px; overflow-x: auto; font-size: 13px;
}
pre.token { color: var(--ok); word-break: break-all; white-space: pre-wrap; }

/* ------------------------------------------------------------- settings */
.theme-picker { display: flex; gap: 10px; flex-wrap: wrap; }
.theme-picker button {
  flex: 0 0 auto; padding: 8px 14px; border-radius: 8px; cursor: pointer; font: inherit;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);
}
.theme-picker button[aria-pressed="true"] { border-color: var(--accent); color: var(--ink); font-weight: 600; }

/* The colour LUTs.  Two boards and a handful of controls in rows, so the only
   thing to undo is the form rule up the file: a <select> there is a field in a
   form and takes the width of its box, and one of these is a choice in a table
   cell beside two columns of text.  The file inputs are labels, the way every
   other picker on the site is, and a label over a hidden input has to say it
   is clickable itself. */
#lutPanel label.btn { cursor: pointer; }
#lutPanel select {
  display: inline-block; width: auto; max-width: 100%; margin-top: 0; padding: 6px 8px;
}
#lutPanel .row .hint { flex: 1 1 auto; }

@media (max-width: 720px) {
  .panel { left: 12px; width: auto; top: auto; height: 45%; }
  /* Collapsed, the panel carries two classes to this rule's one, so its
     `width: 26px` wins and the box is left holding `left`, `width` and
     `right` at the same time.  CSS settles that by ignoring `right`, which
     parks the stub against the left edge of the screen -- the one direction
     its arrow never points.  Letting go of `left` instead sends the stub off
     to the right, where the arrow says it went and where the wide screen has
     always put it. */
  .panel.collapsed { left: auto; }
  /* Now that the button holds its width, the two of them no longer quite fit
     side by side on a phone.  `place.php` is the tight one: its heading is a
     coordinate pair in a monospaced face, which will not go narrower than one
     of the two numbers, and that beside "Back to the map" comes to six pixels
     more than a 320px screen leaves inside the card.  Six is enough to make
     the page scroll sideways, so the row wraps here and whatever is on the
     end of it goes under the heading instead.

     Every row, `report.php`'s included, and that one is a decision rather
     than a spill.  Its `.report-actions` column does shrink, so it never took
     the page sideways -- but it stops at the width of the vote pill, and
     measured at 320px that is 141 of the 238 pixels inside the card.  The
     heading was left the other 104: "Blocking crosswalk" over two lines, the
     ident, the status pill and "filed by ... 3 hours ago" under it over four,
     and both status buttons breaking their labels across the middle of a
     column that is not as wide as they are.

     Wrapped, the title is on one line, the line under it on two, and the
     buttons carry their labels whole.  What that costs is 25 pixels of card
     above the photograph -- 212 against 187 at 320px, both measured -- and
     the vote widget's place in the top right corner, where somebody who came
     to vote looks for it.  Paid, because the widget is still the first thing
     under the meta line and still above the photograph, and because this is
     the page every link on the site leads to and it should be able to say
     what it is about on one line.

     Keeping the column beside the heading and making it narrower was the
     other direction and it is not this page's to take: the 141px is the vote
     pill's own width, and the pill is drawn by the moderation queue and by
     the map's list of reports as well.  Unscoping the rule also stops the
     wrap depending on `:has()`, which was six pixels of sideways scroll for
     any browser without it.

     The column lies down when it lands on its own line -- `moderation.css`,
     which is where it is styled and which this page links for itself. */
  .report-head { flex-wrap: wrap; }
  .queue-item { flex-direction: column; }
  .facts { grid-template-columns: 1fr; }
  /* Stacked on a phone, but the photograph still comes first, for the same
     reason it is on the left of a wide screen. */
  .card:has(> .media-grid) { display: flex; flex-direction: column; }
  .card:has(> .media-grid) > * { order: 4; }
  .card:has(> .media-grid) > .report-head { order: 1; }
  .card:has(> .media-grid) > .media-grid { order: 2; }
  .card:has(> .media-grid) > .facts { order: 3; }
}
