/* Collapses a wide table to cards (uses data-label per <td>). Breakpoint is Bootstrap's `lg` (992px),
   not `md` (768): eldur is used on phones *and* tablets in the field, and multi-column config tables
   (outputs, alarm-group outputs, detector wiring) are unreadable as a real table on a landscape phone
   or a small tablet — so stack up to lg, not just on a portrait phone. */
@media screen and (max-width: 991.98px) {
  /* Drop the table box entirely so each row is a full-width card. Without this, a table that also sets
     `table-layout:fixed` + a <colgroup> (the detector wiring tables, for cross-table alignment on
     desktop) stays a fixed-layout table and crushes every cell into the first column's width — the
     one-word-per-line wrapping seen on a phone. As blocks, table-layout/colgroup no longer apply. */
  .responsive-table, .responsive-table tbody { display: block; width: 100%; }
  .responsive-table colgroup { display: none; }
  .responsive-table thead { display: none; }
  .responsive-table tr {
    display: block;
    margin-bottom: 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.5rem;
    padding: 0.5rem;
    background: var(--bs-body-bg);
  }
  /* Label left, value right; flex-wrap so a long value (e.g. "Output type" = "Fire output (FIRE)")
     drops to a full-width line below its label instead of being crushed into a narrow right sliver. */
  .responsive-table td {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
    text-align: right;
    padding: 0.25rem 0.5rem;
    border: none;
    border-bottom: 1px solid var(--bs-border-color);
  }
  .responsive-table td:last-child { border-bottom: none; }
  .responsive-table td::before {
    content: attr(data-label);
    font-weight: bold;
    text-transform: uppercase;
    font-size: 0.8em;
    color: var(--bs-secondary-color);
    text-align: left;
    white-space: nowrap;
  }
  /* Let the value take the remaining row and wrap within it, never overflow. */
  .responsive-table td > * { min-width: 0; }
}

/* Printable documents (FR-13's detector list is meant to be posted/filed on paper, not just
   viewed): drop the chrome, keep the data. `.no-print` marks controls that make no sense on
   paper (sort/order tabs, the print button itself). */
@media print {
  /* Paper is light, whatever the screen is (owner 2026-08-19). theme.js flips `data-bs-theme` to light
     around the print itself, which is what actually repaints Bootstrap's palette; these two rules are
     the no-JS floor, so a dark-mode print can never come out as pale text on white. */
  :root { color-scheme: light; }
  html, body { background: #fff !important; color: #000 !important; }
  nav.navbar, .no-print { display: none !important; }
  main.container { max-width: 100%; }
  table { font-size: 0.85rem; }
  /* A printed table is a document, not a UI: no link colour, no underlines (some rows carry links and
     some don't, which read as random underlining), and the header repeats on every page (owner
     2026-08-19). Row shading is white/grey alternating and must be FORCED — browsers drop backgrounds
     by default, which is what made the striping come and go. */
  a { color: inherit !important; text-decoration: none !important; }
  thead { display: table-header-group; }
  tfoot { display: table-footer-group; }
  tr, img { page-break-inside: avoid; break-inside: avoid; }
  table.table > tbody > tr > td { border-top: none; border-bottom: none; }
  table.table > tbody > tr:nth-of-type(odd) > td {
    background: #ececec !important;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  /* Bootstrap paints its stripe through a custom property that browsers drop with backgrounds — off it
     goes, and the forced rule above owns the shading. Same rows as on screen (odd), so a print is not
     shaded opposite to the page it came from. */
  table.table-striped > tbody > tr > * { --bs-table-bg-type: initial; box-shadow: none !important; }
  tfoot.print-foot { display: table-footer-group; }
}

/* The repeated print footer (detector list): provenance on every page — printed-at, config epoch and
   the row count the on-screen header no longer prints. It is a table-footer-group, which repeats on
   every printed page and, unlike a `position: fixed` block, occupies real room — a fixed footer
   reserves no space in the flow, so it printed on top of the row that ended the page and clipped it
   (owner 2026-08-19). Hidden on screen. */
tfoot.print-foot { display: none; }
@media print {
  tfoot.print-foot > tr > td {
    padding-top: 4px;
    border-top: 1px solid #999 !important; border-bottom: none;
    background: none !important;
    font-size: 0.7rem; color: #444;
  }
  /* The three fields spaced across the width, without a nested layout table. */
  tfoot.print-foot span + span { margin-left: 1.5rem; }
  /* Page numbering. Chromium implements @page margin boxes; Gecko does not (it prints its own
     footer instead, which the user enables in the print dialog), so this is a progressive
     enhancement, not the only source of a page number. Language-neutral on purpose — CSS content
     can't go through gettext. */
  @page {
    /* The bottom margin only has to house the page counter now that the footer is in the flow. */
    margin: 12mm 10mm 12mm;
    @bottom-right { content: counter(page) " / " counter(pages); font-size: 8pt; color: #444; }
  }
}

/* Time picker (timepicker.js): a discoverable clock button + scrollable list layered over the
   native <input type=time>, because Firefox offers no time popup. The input keeps its own width;
   the button and menu are positioned relative to a thin wrapper. */
.tp-wrap { position: relative; display: inline-flex; align-items: stretch; flex: 1 1 auto; }
.tp-wrap > input[type=time] { flex: 1 1 auto; padding-right: 2rem; }
/* User comment bodies (Markdown → sanitized HTML, TODO #35): tighten block spacing so a comment reads
   as a compact block, not a document. */
.comment-body > :first-child { margin-top: 0; }
.comment-body > :last-child { margin-bottom: 0; }
.comment-body p { margin-bottom: 0.35rem; }
.comment-body ul, .comment-body ol { margin-bottom: 0.35rem; padding-left: 1.25rem; }
.comment-body code { background: var(--bs-tertiary-bg); padding: 0.1rem 0.3rem; border-radius: 0.2rem; }
.comment-body pre {
  background: var(--bs-tertiary-bg); padding: 0.5rem 0.75rem; border-radius: 0.375rem;
  overflow-x: auto; margin-bottom: 0.35rem;
}
.comment-body pre code { background: none; padding: 0; }
.comment-body blockquote {
  border-left: 3px solid var(--bs-border-color); padding-left: 0.75rem;
  color: var(--bs-secondary-color); margin: 0 0 0.35rem;
}

/* datetime-local (Firefox, TODO #63): the native field keeps its own spinner/calendar controls on the
   right, so the clock button sits *beside* the input as a flex child, not overlaid on top of them. */
.tp-wrap.tp-dt > input { flex: 1 1 auto; }
.tp-wrap.tp-dt > .tp-toggle { position: static; align-self: stretch; }
.tp-toggle {
  position: absolute; right: 0; top: 0; bottom: 0; width: 2rem;
  border: none; background: transparent; cursor: pointer; line-height: 1;
  color: var(--bs-secondary-color); padding: 0;
}
.tp-toggle:hover { color: var(--bs-body-color); }
.tp-menu {
  position: absolute; top: calc(100% + 2px); left: 0; z-index: 1060;
  min-width: 100%; max-height: 15rem; overflow-y: auto;
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); padding: 0.25rem 0;
}
.tp-item {
  display: block; width: 100%; text-align: left; border: none; background: transparent;
  padding: 0.25rem 0.75rem; font-variant-numeric: tabular-nums; color: var(--bs-body-color); cursor: pointer;
}
.tp-item:hover { background: var(--bs-tertiary-bg); }
.tp-item.active { background: var(--bs-primary); color: #fff; }

/* Alternating row colour on EVERY table (owner 2026-08-19) — the detector list's look, made the house
   style rather than a per-table opt-in. This is Bootstrap's own `.table-striped` mechanism, not a
   background of our own: setting the two `--bs-table-*-type` custom properties is exactly what that
   class does, so a contextual row (`.table-danger`, `.table-active`) still wins over the stripe, and
   the shade follows the theme in dark mode instead of being pinned to a light grey.
   Opt out with `.table-no-stripe` where a table's rows are paired or already colour-coded. Tables that
   still carry `table-striped` themselves are unaffected — it resolves to the same rule. */
.table:not(.table-no-stripe) > tbody > tr:nth-of-type(odd) > * {
  --bs-table-color-type: var(--bs-table-striped-color);
  --bs-table-bg-type: var(--bs-table-striped-bg);
}

/* Click-to-sort tables (table-sort.js, TODO #25): a header shows it's sortable, and marks the active
   column with a direction arrow. Opt out with class "no-sort" on the <table> or a <th>. */
th.sortable { cursor: pointer; user-select: none; }
th.sortable:hover { background: var(--bs-tertiary-bg); }
.sort-ind { font-size: 0.8em; opacity: 0.7; }

/* Type-badge filter (TODO #24/#30): a clickable badge that highlights when active. */
.type-badge { cursor: pointer; }
.type-badge.active {
  background: var(--bs-primary) !important; color: #fff !important; border-color: var(--bs-primary) !important;
}

/* Collapsible section header caret (TODO #30): points down when open, right when collapsed. */
.collapse-caret { display: inline-block; transition: transform 0.15s; }
a.collapsed .collapse-caret, button.collapsed .collapse-caret { transform: rotate(-90deg); }

/* Deep-linked comment (from the /comments overview → its page anchor, TODO #21). Keep the anchor
   clear of the top edge on scroll, and flash it briefly so the reader sees which one was linked. */
.comment-anchor { scroll-margin-top: 1rem; }
.comment-anchor:target { animation: comment-flash 2s ease-out 1; border-radius: 0.375rem; }
@keyframes comment-flash {
  0%, 30% { background: var(--bs-warning-bg-subtle, #fff3cd); }
  100% { background: transparent; }
}

/* /log live indicator (owner 2026-08-05): a pulsing green dot marks the newest-first page 1 as
   real-time; the badge flashes when a fresh row-swap actually lands. Both quiet under reduced-motion. */
.log-live-dot {
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--bs-success, #198754); display: inline-block;
  animation: log-live-pulse 1.6s ease-in-out infinite;
}
@keyframes log-live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
.log-live-flash { animation: log-live-flash .6s ease-out 1; }
@keyframes log-live-flash {
  0% { background: var(--bs-success); color: #fff; }
  100% { /* settle back to the badge's own bg-success-subtle */ }
}
@media (prefers-reduced-motion: reduce) {
  .log-live-dot, .log-live-flash { animation: none; }
}

/* Dark-mode map excerpts are now baked server-side (a `?theme=dark` render inverts the raster before
   drawing the overlays, so the condition colours stay true — the old CSS `img.map-raster` invert
   mangled those baked hues). The theme is hydrated onto each `data-themed` <img> by type-filter.js. */

/* Dark mode: `.btn-outline-dark` is dark-on-dark → invisible (bit the "Deem state correct" button on
   the live-state banner + silence-gaps page). Bootstrap 5.3 doesn't auto-flip button variants per
   theme, so retint this one to a light outline via its own CSS vars — legible in both themes (app-wide,
   not just the deem buttons). Owner 2026-08-02. */
[data-bs-theme="dark"] .btn-outline-dark {
  --bs-btn-color: #dee2e6;
  --bs-btn-border-color: #adb5bd;
  --bs-btn-hover-bg: #dee2e6;
  --bs-btn-hover-color: #212529;
  --bs-btn-hover-border-color: #dee2e6;
  --bs-btn-active-bg: #dee2e6;
  --bs-btn-active-color: #212529;
  --bs-btn-active-border-color: #dee2e6;
  --bs-btn-disabled-color: #6c757d;
  --bs-btn-disabled-border-color: #495057;
}

/* Dark mode: the default outline-primary (blue) and outline-secondary (grey) render too dim on the
   dark body — the text + border sit near-black on near-black. Retint to the lighter accents ztar uses
   so they read (owner 2026-08-09). Same CSS-var pattern as the outline-dark fix above. */
[data-bs-theme="dark"] .btn-outline-primary {
  --bs-btn-color: #6ea8fe;
  --bs-btn-border-color: #6ea8fe;
  --bs-btn-hover-color: #000;
  --bs-btn-hover-bg: #6ea8fe;
  --bs-btn-hover-border-color: #6ea8fe;
  --bs-btn-active-color: #000;
  --bs-btn-active-bg: #6ea8fe;
  --bs-btn-active-border-color: #6ea8fe;
}
[data-bs-theme="dark"] .btn-outline-secondary {
  --bs-btn-color: #adb5bd;
  --bs-btn-border-color: #6c757d;
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #495057;
  --bs-btn-hover-border-color: #adb5bd;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #495057;
  --bs-btn-active-border-color: #adb5bd;
}
/* Solid primary (blue): Bootstrap's dark primary keeps white text on a mid-blue that reads a touch
   muddy — brighten the fill + darken the text so the label pops (matches the outline accent). */
[data-bs-theme="dark"] .btn-primary {
  --bs-btn-bg: #6ea8fe;
  --bs-btn-border-color: #6ea8fe;
  --bs-btn-color: #000;
  --bs-btn-hover-bg: #8bb9fe;
  --bs-btn-hover-border-color: #8bb9fe;
  --bs-btn-hover-color: #000;
  --bs-btn-active-bg: #8bb9fe;
  --bs-btn-active-border-color: #8bb9fe;
  --bs-btn-active-color: #000;
}

/* Searchable combo box (combobox.js) — used by the fault-trend loop/detector pickers. Theme-aware via
   Bootstrap CSS vars so it reads in light and dark. */
.eldur-combo { position: relative; }
.eldur-combo .combo-list {
  position: absolute; z-index: 1050; left: 0; right: 0; top: 100%; margin: 0; padding: 0;
  list-style: none; max-height: 16rem; overflow-y: auto;
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color);
  border-radius: 0 0 .375rem .375rem; box-shadow: 0 .5rem 1rem rgba(0,0,0,.15);
}
.eldur-combo .combo-opt {
  display: flex; gap: .5rem; align-items: baseline; padding: .25rem .5rem; cursor: pointer;
  white-space: nowrap; overflow: hidden;
}
.eldur-combo .combo-opt:hover, .eldur-combo .combo-opt.active { background: var(--bs-tertiary-bg); }
.eldur-combo .combo-main { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
.eldur-combo .combo-sub { flex: 1 1 auto; min-width: 0; color: var(--bs-secondary-color);
  overflow: hidden; text-overflow: ellipsis; }
.eldur-combo .combo-input { padding-right: 1.6rem; }
.eldur-combo .combo-clear {
  position: absolute; top: 0; right: 0; height: 100%; width: 1.6rem; padding: 0;
  border: 0; background: transparent; line-height: 1; font-size: 1.1rem;
  color: var(--bs-secondary-color); cursor: pointer;
}
.eldur-combo .combo-clear:hover { color: var(--bs-body-color); }
.eldur-combo .combo-more { padding: .25rem .5rem; color: var(--bs-secondary-color);
  font-size: .8rem; font-style: italic; }

/* Ranked horizontal-bar breakdown (_plots.html hbars) — fault-code / fault-type reports. */
.hbar-table .hbar-label { width: 14rem; max-width: 40vw; }
.hbar-table .hbar-cell { width: 60%; }
.hbar { background: var(--bs-tertiary-bg); border-radius: .25rem; overflow: hidden; }
.hbar > span { display: block; height: .8rem; border-radius: .25rem; }

/* ── Condition Gantt (_gantt.html; TODO #34/#47) — a lane per entity, a bar per active interval on a
   shared time axis. Theme-aware via Bootstrap vars; bar fills are the map's condition colours.
   Interactive since 2026-08-18 (gantt.js): sticky axis + legend, zoom/pan, fullscreen, filtering. ── */
.eldur-gantt { font-size: .8rem; }
.eldur-gantt .gantt-box { position: relative; }
/* overflow-x CLIP, not hidden: hidden would make this a scroll container and kill the sticky axis,
   while clip still stops a trailing marker label from spilling out of the border (owner 2026-08-18). */
.eldur-gantt .gantt-scroll { border: 1px solid var(--bs-border-color); border-radius: .375rem;
  overflow-x: clip; overflow-y: visible; position: relative; }
/* Click-to-timestamp pick (owner 2026-08-07): a full-height red line + a wall-clock flag, dropped by
   gantt-pick.js while the "Pick on timeline" toggle is on — and by gantt.js's plain-click crosshair. */
.eldur-gantt.gantt-picking, .eldur-gantt.gantt-picking .gantt-track { cursor: crosshair; }
.gantt-pick-line { position: absolute; top: 0; width: 2px; background: #e53935; z-index: 6; pointer-events: none; }
.gantt-pick-flag { position: absolute; top: 0; left: 2px; background: #e53935; color: #fff;
  font-size: .7rem; line-height: 1.3; padding: 0 .3rem; white-space: nowrap; border-radius: 0 3px 3px 0; }
.eldur-gantt .gantt-row { display: flex; align-items: stretch; border-top: 1px solid var(--bs-border-color); }
.eldur-gantt .gantt-row:first-child { border-top: 0; }
.eldur-gantt .gantt-side {
  flex: 0 0 6.5rem; width: 6.5rem; padding: .2rem .45rem; display: flex; align-items: center;
  font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: var(--bs-tertiary-bg); border-right: 1px solid var(--bs-border-color);
}
.eldur-gantt .gantt-track { position: relative; flex: 1 1 auto; min-height: 22px; }
/* Axis row. It only needs to float in FULL SCREEN: outside it the timeline is capped to the viewport
   and never scrolls inside itself, so the axis is on screen already (owner 2026-08-18). */
.eldur-gantt.gantt-full .gantt-axis { position: sticky; top: 0; z-index: 4; background: var(--bs-body-bg); }
.eldur-gantt .gantt-axis .gantt-side { background: var(--bs-body-bg); border-right-color: transparent; }
.eldur-gantt .gantt-axis .gantt-track { min-height: 20px; }
.eldur-gantt .gantt-tick { position: absolute; top: 0; bottom: 0; transform: translateX(-50%); }
.eldur-gantt .gantt-tick-label { position: absolute; top: 3px; left: 50%; transform: translateX(-50%);
  color: var(--bs-secondary-color); font-variant-numeric: tabular-nums; white-space: nowrap; }
.eldur-gantt .gantt-grid { position: absolute; top: 0; bottom: 0; width: 0;
  border-left: 1px dashed var(--bs-border-color); opacity: .5; }
/* Bars */
.eldur-gantt .gantt-bar {
  position: absolute; height: 18px; border-radius: .2rem; padding: 0 .3rem; overflow: hidden;
  display: flex; align-items: center; box-shadow: 0 0 0 1px rgba(0,0,0,.15) inset;
  text-decoration: none;  /* it is an <a> when it links to its raising event */
}
a.eldur-gantt .gantt-bar, .eldur-gantt a.gantt-bar:hover { filter: brightness(1.08); }
.eldur-gantt a.gantt-bar { cursor: pointer; }
.eldur-gantt div.gantt-bar { cursor: default; }
.eldur-gantt .gantt-bar-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: .72rem; line-height: 1; }
/* Ongoing bars get a faint diagonal hatch on the trailing edge to read as "still running". A bar that
   merely runs past the ZOOMED window's edge gets the same treatment while you are looking at that
   window: it continues past what you can see, which is the same statement at that scale. */
.eldur-gantt .gantt-bar[style*="--gantt-ongoing"], .eldur-gantt .gantt-bar.gantt-runs-on {
  background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.35) 0 4px, transparent 4px 8px);
  background-blend-mode: overlay;
}
.eldur-gantt .gantt-legend { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center;
  margin-top: .4rem; font-size: .75rem; background: var(--bs-body-bg); padding: .2rem 0; }
.eldur-gantt.gantt-full .gantt-legend { position: sticky; bottom: 0; z-index: 4; }
/* A legend key is a filter button (owner 2026-08-18) — switched off, it dims and its lanes collapse. */
.eldur-gantt .gantt-key { display: inline-flex; align-items: center; gap: .3rem;
  background: none; border: 0; padding: .1rem .2rem; color: inherit; border-radius: .2rem; cursor: pointer; }
.eldur-gantt .gantt-key:hover { background: var(--bs-tertiary-bg); }
.eldur-gantt .gantt-key-off { opacity: .4; text-decoration: line-through; }
.eldur-gantt .gantt-swatch { width: .8rem; height: .8rem; border-radius: .2rem; display: inline-block;
  box-shadow: 0 0 0 1px rgba(0,0,0,.2) inset; }
.eldur-gantt .gantt-frame { margin-left: auto; font-variant-numeric: tabular-nums; }
.eldur-gantt .gantt-hint { font-size: .7rem; margin-top: .15rem; }
/* Zero-duration events → a diamond marker (a point in time) with a trailing label. Diamond and label
   are ONE positioned element so the script can move a marker to another lane and re-pack it as a unit
   (owner 2026-08-18). The diamond straddles its instant; the label trails to the right of it. */
.eldur-gantt .gantt-mark { position: absolute; display: inline-flex; align-items: center;
  white-space: nowrap; text-decoration: none; line-height: 18px; }
.eldur-gantt .gantt-mark .gantt-marker {
  flex: 0 0 auto; width: 12px; height: 12px; margin-left: -6px; transform: rotate(45deg);
  border-radius: 2px; box-shadow: 0 0 0 1px rgba(0,0,0,.2) inset;
}
.eldur-gantt .gantt-mark .gantt-marker-label { margin-left: 8px; font-size: .72rem;
  color: var(--bs-secondary-color);
  /* Ellipsis for the one case a label can't be placed either way (gantt.js caps its max-width there).
     `min-width: 0` is what lets a flex item shrink below its text width at all. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Near the right edge the label would be clipped away by `.gantt-scroll`'s overflow-x: clip, leaving a
   diamond with no text — which is how an episode's CLOSING event, always the last thing in the frame,
   went missing from every incident timeline (owner 2026-08-19). Flipped, the label trails leftward.
   `translateX(-100%)` puts the box's right edge on the instant and the diamond's negative right margin
   lets it straddle that instant exactly as the unflipped one straddles it on the left. */
.eldur-gantt .gantt-mark-flip { flex-direction: row-reverse; transform: translateX(-100%); }
.eldur-gantt .gantt-mark-flip .gantt-marker { margin-left: 0; margin-right: -6px; }
.eldur-gantt .gantt-mark-flip .gantt-marker-label { margin-left: 0; margin-right: 8px; }
.eldur-gantt a.gantt-mark .gantt-marker-label { color: var(--bs-body-color); }
.eldur-gantt a.gantt-mark:hover .gantt-marker-label { text-decoration: underline; }
.eldur-gantt .gantt-row-marker .gantt-side { font-variant-numeric: normal; }
/* The truncating element inside the (flex) side cell — see the note in _gantt.html. `min-width: 0` is
   what lets a flex item shrink below its text width at all; without it the ellipsis never appears. */
.eldur-gantt .gantt-side-text {
  min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.eldur-gantt .gantt-side-link { color: inherit; text-decoration: none; }
.eldur-gantt .gantt-side-link:hover { text-decoration: underline; }

/* Navigation (gantt.js): drag pans, Shift+drag draws the zoom band. */
.eldur-gantt.gantt-dragging .gantt-track { cursor: grabbing; }
.eldur-gantt .gantt-band { position: absolute; top: 0; z-index: 5; pointer-events: none;
  background: rgba(30,136,229,.18); border-left: 1px solid #1e88e5; border-right: 1px solid #1e88e5; }
/* Controls: one right-aligned row ABOVE the frame — cogwheel, then full screen (owner 2026-08-18).
   They floated over the drawing area first, which put the cogwheel on top of the legend's frame
   caption; nothing overlaps the timeline now, at any width. */
.eldur-gantt .gantt-controls { display: flex; justify-content: flex-end; align-items: center;
  gap: .3rem; margin-bottom: .25rem; }
.eldur-gantt .gantt-icon {
  width: 1.9rem; height: 1.9rem; padding: 0; line-height: 1; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  background: var(--bs-body-bg); color: var(--bs-body-color);
  border: 1px solid var(--bs-border-color); border-radius: .3rem;
}
.eldur-gantt .gantt-icon:hover { background: var(--bs-tertiary-bg); }
.eldur-gantt .gantt-tools { position: relative; }
/* The panel hangs BELOW its button now that the button sits above the frame — and it must float over
   the timeline rather than push it down, or opening the options would reflow everything. */
/* Above `.alarm-bottombar` (z-index 30): on /alarm the timeline sits over the sticky summary bar, and
   at 8 the options panel opened *behind* it — the bar covered the lower half of the checkbox list
   (owner 2026-08-19). Still well below the modal/overlay range (1050+). */
.eldur-gantt .gantt-tools-panel {
  position: absolute; right: 0; top: 2.2rem; z-index: 40; width: 17rem; padding: .55rem .65rem;
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: .375rem;
  box-shadow: 0 2px 10px rgba(0,0,0,.25); font-size: .78rem;
}
.eldur-gantt .gantt-tools-title { font-weight: 600; margin-bottom: .35rem; }
.eldur-gantt .gantt-tool { display: flex; align-items: flex-start; gap: .4rem; cursor: pointer; }
.eldur-gantt .gantt-tool-help { color: var(--bs-secondary-color); font-size: .72rem; margin-top: .2rem; }
/* Fullscreen: a CSS overlay rather than the Fullscreen API — it keeps the page's theme and chrome, and
   works inside an embedded/iframed view where the API is blocked (owner 2026-08-18). The view window is
   plain state, so the slice you were reading is the slice you get, only larger. */
.eldur-gantt.gantt-full {
  position: fixed; inset: 0; z-index: 1080; margin: 0; padding: .6rem .8rem;
  background: var(--bs-body-bg); overflow: hidden; display: flex; flex-direction: column;
}
.eldur-gantt.gantt-full .gantt-box { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.eldur-gantt.gantt-full .gantt-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
body.gantt-full-open { overflow: hidden; }
/* Capped height outside full screen (owner 2026-08-18): a super-tall timeline is cut off rather than
   given its own scrollbar — scrolling a small box is the thing full screen replaced. The cap leaves
   room for the controls, legend and hint so the whole component still fits a viewport. */
.eldur-gantt:not(.gantt-full) .gantt-scroll { max-height: 62vh; overflow: hidden; }
/* The cut-off has to be unmissable, or a reader takes the last visible lane for the last lane. */
.eldur-gantt .gantt-more {
  display: flex; align-items: center; justify-content: center; gap: .4rem; width: 100%;
  margin-top: -1px; padding: .3rem .5rem; font-size: .78rem; font-weight: 600; cursor: pointer;
  color: var(--bs-body-color); background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color); border-top-style: dashed;
  border-radius: 0 0 .375rem .375rem;
}
.eldur-gantt .gantt-more:hover { background: var(--bs-secondary-bg); text-decoration: underline; }
.eldur-gantt .gantt-more-arrow { font-size: 1.1rem; line-height: 1; }
/* …and the lanes fade into it, so the cut never reads as "the data stops here". */
.eldur-gantt:not(.gantt-full) .gantt-scroll.gantt-cut::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2.2rem; pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--bs-body-bg));
}

/* Pointer capture (owner 2026-08-18): dragging across the timeline was starting a native link/image
   drag or a text selection instead of panning. Links here are targets, never draggable payloads. */
.eldur-gantt .gantt-scroll { user-select: none; -webkit-user-select: none; touch-action: pan-y; }
.eldur-gantt .gantt-scroll a, .eldur-gantt .gantt-scroll img {
  -webkit-user-drag: none; user-drag: none; }

/* As-of map slider dots (owner 2026-08-06): the visible dot stays 7px and aligned to its notch, but a
   larger transparent ::before extends the hover/hit target so the native title tooltip is easy to
   trigger. Absolutely positioned → out of flow, so it never shifts the dots' justify-between spacing. */
.map-slider-dot { position: relative; }
.map-slider-dot::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 22px; height: 22px; transform: translate(-50%, -50%);
  border-radius: 50%; /* transparent — a bigger pointer target only */
}

/* Duty coverage gantt (owner 2026-08-07): one lane per user, %-positioned bars over the horizon. Uses
   Bootstrap theme vars so it adapts to light/dark without a separate rule. */
.duty-gantt { font-size: .8rem; }
.duty-lane { display: flex; align-items: center; gap: .5rem; padding: .12rem 0; }
.duty-name { flex: 0 0 9rem; max-width: 9rem; }
.duty-track {
  position: relative; flex: 1 1 auto; height: 1.5rem;
  background: var(--bs-tertiary-bg); border-radius: 4px;
}
.duty-axis .duty-track { height: 1.1rem; background: transparent; }
.duty-tick { position: absolute; top: 0; transform: translateX(-50%); color: var(--bs-secondary-color); white-space: nowrap; }
.duty-grid { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--bs-border-color); opacity: .5; }
.duty-bar {
  position: absolute; top: .28rem; height: .94rem; min-width: 3px; border-radius: 3px;
  border: 1px solid var(--bs-primary); background: transparent;  /* scheduled = outlined */
}
.duty-bar.live { background: var(--bs-success); border-color: var(--bs-success); }  /* on duty now = solid */
/* Open-ended / past-the-horizon shift: hatch the bar so it reads as "still running" — matches the
   condition/availability gantts' ongoing treatment (owner 2026-08-09, TODO #39). */
.duty-bar.ongoing {
  background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.35) 0 4px, transparent 4px 8px);
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}
.duty-bar.ongoing:not(.live) {
  background-color: color-mix(in srgb, var(--bs-primary) 22%, transparent);
}
.duty-dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; background: var(--bs-success); margin-right: .3rem; }

/* ── Alarm page redesign (owner 2026-08-07): compact, responder-first. Situation left, action right on
   ≥lg; single column stacked on mobile. Bootstrap theme vars → auto light/dark. ── */
.alarm-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@media (min-width: 992px) {
  .alarm-grid { grid-template-columns: 1.35fr 1fr; align-items: start; }
  .alarm-action { position: sticky; top: 1rem; }
}
.alarm-situation, .alarm-action { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }

.alarm-banner { position: relative; border-radius: .75rem; padding: .9rem 1rem; text-align: center; }
.alarm-banner.is-alarm { background: var(--bs-danger); color: #fff; }
.alarm-banner.is-pre { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis); }
.alarm-banner.is-reset { background: var(--bs-secondary); color: #fff; }  /* alarm reset → greyed (owner 2026-08-07) */
.alarm-banner .ab-kind { font-size: .72rem; text-transform: uppercase; letter-spacing: .12em; font-weight: 800; opacity: .93; }
.alarm-banner .ab-title { font-weight: 800; line-height: 1.05; margin: .15rem 0 .1rem; white-space: nowrap;
                          overflow: hidden; font-size: 1.9rem; }
.alarm-banner .ab-line2 { font-size: 1.02rem; font-weight: 600; }
.alarm-banner .ab-line2 b { font-variant-numeric: tabular-nums; }
.alarm-banner .ab-line2 a { color: inherit; }
.alarm-banner .ab-sub { font-size: .8rem; opacity: .9; margin-top: .05rem; }
.alarm-banner .ab-sub a { color: inherit; }
/* detector-type tile, top-right of the banner — visible whenever the type is known (owner 2026-08-07) */
.alarm-banner .ab-type { position: absolute; top: .5rem; right: .5rem; display: flex; flex-direction: column;
  align-items: center; gap: .1rem; background: rgba(255,255,255,.16); border-radius: .5rem; padding: .25rem .4rem;
  max-width: 6rem; }
.alarm-banner.is-pre .ab-type { background: rgba(0,0,0,.06); }
.alarm-banner .ab-type img { width: 26px; height: 26px; object-fit: contain; }
.alarm-banner .ab-type span { font-size: .58rem; text-transform: uppercase; letter-spacing: .03em; font-weight: 700;
  line-height: 1.05; text-align: center; }

/* coordination — four tone buttons, 2×2, read by colour + glyph + position */
.coord-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
/* The coordination timeline caps its height and scrolls only once it's long (owner 2026-08-08: after
   ten messages), so a busy incident doesn't push the message box off-screen. */
#timeline.coord-scroll { max-height: 22rem; overflow-y: auto; }
/* Up/down "more above / more below" indicators over the scroll box (owner 2026-08-08): map-arrow shape
   but a translucent circle, so they don't obscure the messages. The wrap is the positioning context;
   the arrows sit OUTSIDE #timeline so they don't scroll with it. Shown by the script only when scrollable
   and there's content that way; clickable to page through. */
.coord-scroll-wrap { position: relative; }
.coord-scroll-wrap .coord-arrow { position: absolute; left: 50%; transform: translateX(-50%); z-index: 3;
  width: 1.6rem; height: 1.6rem; border: 0; border-radius: 50%; padding: 0; line-height: 1;
  font-size: .7rem; cursor: pointer; color: var(--bs-body-color);
  background: color-mix(in srgb, var(--bs-secondary) 30%, transparent); }
.coord-scroll-wrap .coord-arrow.coord-up { top: .1rem; }
.coord-scroll-wrap .coord-arrow.coord-down { bottom: .1rem; }
@media (hover: hover) { .coord-scroll-wrap .coord-arrow:hover {
  background: color-mix(in srgb, var(--bs-secondary) 55%, transparent); } }
.coord-btn { border: 0; border-radius: .7rem; padding: .8rem .5rem; cursor: pointer; color: #fff;
  display: flex; flex-direction: column; align-items: center; gap: .15rem; text-align: center; min-height: 4.2rem;
  justify-content: center; width: 100%; }
.coord-btn .ico { font-size: 1.45rem; line-height: 1; }
.coord-btn .lb { font-weight: 800; font-size: .9rem; }
.coord-btn .hint { font-size: .72rem; opacity: .93; font-weight: 500; display: none; }
.coord.show-hints .coord-btn .hint { display: block; }
.coord-btn.tone-fire { background: var(--bs-danger); }
.coord-btn.tone-brigade { background: var(--bs-warning); color: #1a1206; }
.coord-btn.tone-reset { background: var(--bs-success); }
.coord-btn.tone-false { background: var(--bs-secondary); }
.coord-btn:active { transform: scale(.97); }
.coord-hint-toggle { border: 1px solid var(--bs-border-color); background: var(--bs-body-bg);
  color: var(--bs-secondary-color); border-radius: 999px; width: 1.7rem; height: 1.7rem; font-weight: 800; cursor: pointer; }

/* role chip (shown once you hold a role — the claim menu collapses) */
.role-chip { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; font-size: .85rem; }
.role-chip .yours { background: var(--bs-success); color: #fff; border-radius: 999px; padding: .1rem .6rem; font-weight: 700; }

/* Role block: claim menu shows while you hold none; collapses to the chip once you do (owner 2026-08-07). */
#roleBlock .role-chip { display: none; }
#roleBlock.you-hold .roles-pre { display: none; }
#roleBlock.you-hold .role-chip { display: flex; }

/* ── Alarm page v2 (owner 2026-08-07): nav bar, single timer, map carousel, bottom summary, confirm. ── */
.alarm-banner .ab-line2 a { text-decoration: none; }               /* addr: no underline (owner) */
.alarm-banner .ab-sym { width: 20px; height: 20px; object-fit: contain; vertical-align: -3px; margin-right: .3rem; }

/* top nav bar — two half-width buttons to the prev/next incident, coloured by that incident's state */
/* Slim detector selector under the big box (D57, owner 2026-08-08): one button per affected detector.
   A horizontal row that scrolls when it can't fit (touch + overflow), matching the map-excerpt feel. */
.incident-selector-wrap { position: relative; }
.incident-selector { display: flex; gap: .4rem; margin: 0; overflow-x: auto;
  scrollbar-width: thin; -webkit-overflow-scrolling: touch; padding: 3px 3px .3rem; scroll-behavior: smooth; }
/* Overview button — always leftmost, a solid neutral that reads as "not an alarm kind": dark in light
   mode, light in dark mode (owner 2026-08-08). */
.isel-btn.isel-overview { background: var(--bs-body-color); color: var(--bs-body-bg); }
/* End-scroll arrows for the detector row (mirrors the map carousel), shown only when it overflows. */
.isel-arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 3; width: 1.6rem;
  height: 1.6rem; border-radius: 50%; border: 0; padding: 0; line-height: 1; font-size: 1.1rem;
  font-weight: 800; color: #fff; background: rgba(0, 0, 0, .55); cursor: pointer; }
.isel-arrow.isel-prev { left: -.2rem; }
.isel-arrow.isel-next { right: -.2rem; }
.isel-btn { flex: 0 0 auto; display: inline-flex; align-items: center; gap: .35rem; min-width: 5.5rem;
  padding: .3rem .6rem; border-radius: .5rem; font-weight: 700; font-size: .9rem; line-height: 1.4;
  text-decoration: none; border: 3px solid transparent; color: var(--bs-body-color);
  background: var(--bs-secondary-bg); }
.isel-btn img { width: 22px; height: 22px; object-fit: contain; flex: 0 0 auto; }
.isel-btn .isel-addr { font-variant-numeric: tabular-nums; }
.isel-btn.state-alarm { background: var(--bs-danger); color: #fff; }
.isel-btn.state-pre_alarm { background: var(--bs-warning); color: #1a1206; }
.isel-btn.state-reset { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); }
/* The selected detector (or Overview) — its border is a much LIGHTER tint of the button's own state
   colour (owner 2026-08-08), so the ring reads as "this one" without the harsh emphasis-colour outline.
   Drawn on the button's own 3px border (never an outward outline, which the overflow-x row would clip).
   color-mix toward white lightens in either theme; the fills are fixed colours. */
.isel-btn.selected.state-alarm { border-color: color-mix(in srgb, var(--bs-danger) 35%, #fff); }
.isel-btn.selected.state-pre_alarm { border-color: color-mix(in srgb, var(--bs-warning) 35%, #fff); }
.isel-btn.selected.state-reset { border-color: color-mix(in srgb, var(--bs-secondary) 45%, var(--bs-body-bg)); }
.isel-btn.selected.isel-overview { border-color: color-mix(in srgb, var(--bs-body-color) 45%, var(--bs-body-bg)); }
/* A detector that arrived AFTER page load pulses until the responder clicks it (owner 2026-08-08): it
   must not steal focus from the event being worked, but must be impossible to miss. Same beat as the
   lower-bar flash. */
/* A newly-arrived (or re-fired) detector radiates a "halo breathe" until clicked (owner 2026-08-08): a
   soft WHITE glow spreads outward from the object's edge and fades, ~1.7 s, leaving the button's own
   border + fill untouched. Drawn as an animated box-shadow — crucially it does NOT affect layout or
   scrollable width (a transform-scaled pseudo did, which blew the full-width bottom bar past the viewport
   on mobile and added phantom scroll to the overflow-x detector row). The SAME halo is used on the
   bottom-bar new-alarm button; the overflow-x selector row clips the glow top/bottom but its width is
   unaffected. */
.isel-btn.pulse, .alarm-bottombar .bb-newalarm {
  animation: wave-halo 1.7s ease-out infinite; }
@keyframes wave-halo {
  0%   { box-shadow: 0 0 4px 0   rgba(255, 255, 255, .6); }
  70%  { box-shadow: 0 0 4px 13px rgba(255, 255, 255, 0); }
  100% { box-shadow: 0 0 4px 0   rgba(255, 255, 255, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .isel-btn.pulse, .alarm-bottombar .bb-newalarm {
    animation: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7); }
}

/* Head group (owner 2026-08-08): banner, selector, timer and map sit tight together — a smaller gap
   than the situation column's, so the responder's eye stays in the "what + where + map" block. */
.alarm-head { display: flex; flex-direction: column; gap: .45rem; }

/* Unmissable new-incident popup: a red-topped modal on the shared confirm-overlay. */
.incident-overlay .confirm-box { border-top: 5px solid var(--bs-danger); }
.incident-overlay .inc-pop-head { font-weight: 800; color: var(--bs-danger); margin-bottom: .5rem; }

/* single countdown */
.alarm-timer { display: flex; align-items: baseline; justify-content: center; gap: .6rem;
  border-radius: .6rem; padding: .5rem .9rem; border: 1px solid var(--bs-border-color); }
/* Both the hotel/verification "Ack. within" and the "Full alarm in" COUNTDOWN are warning (amber): the
   full alarm hasn't landed yet, so red is premature (owner 2026-08-10). Red is reserved for the static
   "Full alarm" state below — the timer expired or the condition IS a full alarm. */
.alarm-timer.is-ack,
.alarm-timer.is-full { border-color: var(--bs-warning); background: var(--bs-warning-bg-subtle); }
.alarm-timer .at-label { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 700; color: var(--bs-secondary-color); }
.alarm-timer .at-val { font-size: 1.6rem; font-weight: 800; font-variant-numeric: tabular-nums; font-family: var(--bs-font-monospace); }
/* Static "Full alarm" (no countdown left) — label only, so centre it and give it weight. Red: this is
   the actual full-alarm state (expired / condition is FULL ALARM). */
.alarm-timer.is-full-static { justify-content: center;
  border-color: var(--bs-danger); background: var(--bs-danger-bg-subtle); }
.alarm-timer.is-full-static .at-label { font-size: 1rem; color: var(--bs-danger); }

/* map carousel — one excerpt at a time. The slides OVERLAY in a single grid cell, so the track is always
   as tall as the TALLEST slide and switching the active one changes opacity only — the carousel height
   never changes, so the page never relayouts and never scroll-jumps on navigation (owner 2026-08-09; the
   old display:none toggle resized the carousel per slide, which made the browser scroll the page — via
   scroll-anchoring, and via chasing an off-screen focused <summary>). Trade-off: a short close-up now
   shows in a box sized to the tall whole-floor slide. */
.alarm-carousel { position: relative; overflow: hidden; }
.alarm-carousel .ac-track { position: relative; display: grid; }
.alarm-carousel .ac-slide { grid-area: 1 / 1; opacity: 0; pointer-events: none; text-decoration: none; }
.alarm-carousel .ac-slide.active { opacity: 1; pointer-events: auto; animation: ac-slide-in .28s ease; }
@keyframes ac-slide-in { from { opacity: .35; transform: translateX(var(--ac-dir, 0)); } to { opacity: 1; transform: none; } }
/* large opaque nav arrows (injected by JS) */
.alarm-carousel .ac-arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 5;
  width: 2.6rem; height: 2.6rem; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(0,0,0,.55); color: #fff; font-size: 1.5rem; line-height: 1;
  display: flex; align-items: center; justify-content: center; }
.alarm-carousel .ac-arrow.ac-prev { left: .5rem; }
.alarm-carousel .ac-arrow.ac-next { right: .5rem; }
/* Hidden at the ends via a class, NOT the `disabled` attribute (owner 2026-08-09): a focused button
   going disabled (or display:none) drops focus to <body>, which scrolls the page to the top. opacity +
   pointer-events keeps the element in the DOM and focusable, so no focus loss, no scroll jump. */
.alarm-carousel .ac-arrow.ac-hide { opacity: 0; pointer-events: none; }
/* Wide edge click-zones (owner 2026-08-08): a sizeable strip on each side, covering the arrow, also
   navigates. Invisible until hovered, then a grey overlay fading inward reveals the zone. Sits under the
   arrow (z-5) but over the slide, so the map link stays clickable in the centre. */
.alarm-carousel .ac-edge { position: absolute; top: 0; bottom: 0; z-index: 4; border: 0; padding: 0;
  width: clamp(3rem, 14%, 5.5rem); background: transparent; cursor: pointer; transition: background .15s; }
.alarm-carousel .ac-edge.ac-edge-prev { left: 0; }
.alarm-carousel .ac-edge.ac-edge-next { right: 0; }
.alarm-carousel .ac-edge.ac-edge-prev:hover { background: linear-gradient(to right, rgba(90,90,90,.4), transparent); }
.alarm-carousel .ac-edge.ac-edge-next:hover { background: linear-gradient(to left, rgba(90,90,90,.4), transparent); }
.alarm-carousel .ac-edge.ac-hide { opacity: 0; pointer-events: none; }  /* see .ac-arrow.ac-hide — avoids the focus-loss scroll */
@media (hover: none) { .alarm-carousel .ac-edge { display: none; } }  /* touch uses swipe, not edge taps */
.alarm-carousel .ac-slide img { display: block; width: 100%; height: auto; user-select: none; }
.alarm-carousel .ac-cap { position: absolute; left: .5rem; top: .5rem; z-index: 6; pointer-events: none;
  font-size: .72rem;
  background: color-mix(in srgb, var(--bs-body-bg) 82%, transparent); color: var(--bs-secondary-color);
  border: 1px solid var(--bs-border-color); border-radius: .4rem; padding: .1rem .45rem; }
.alarm-carousel .ac-dots { display: flex; justify-content: center; gap: .4rem; padding: .4rem 0; }
.alarm-carousel .ac-dots button { width: .55rem; height: .55rem; border-radius: 50%; border: 0; padding: 0;
  background: var(--bs-border-color); cursor: pointer; }
.alarm-carousel .ac-dots button.on { background: var(--bs-primary); }

/* EXPERIMENT (owner 2026-08-09): embedded interactive map on /alarm (chosen via a /preferences pref). */
.alarm-embed-wrap { overflow: hidden; padding: 0; }
.alarm-embed-map { display: block; width: 100%; height: clamp(320px, 55vh, 620px); border: 0; }

/* Active-incident MOBILE declutter (owner 2026-08-09): hide the navbar to reclaim vertical space on the
   /alarm coordination surface; a fixed (non-scrolling) flame button toggles it back. Desktop and
   off-incident pages are untouched — the toggle is display:none outside the mobile query. */
.incident-nav-toggle { display: none; }
@media (max-width: 768px) {
  body.incident-focus .app-navwrap { display: none; }
  body.incident-focus.nav-shown .app-navwrap { display: block; }
  body.incident-focus .incident-nav-toggle {
    display: inline-flex; position: fixed; top: 5px; left: 5px; z-index: 1050;
    width: 36px; height: 36px; align-items: center; justify-content: center; padding: 0;
    background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: 10px;
    box-shadow: 0 1px 5px rgba(0, 0, 0, .35); cursor: pointer; }
  body.incident-focus .incident-nav-toggle img { width: 24px; height: 24px; display: block; }
  /* Tight, near-full-bleed on mobile during an incident: reclaim the side gutters and the top gap. The
     button sits top-LEFT (owner 2026-08-09) — when the navbar is shown it overlaps the logo, never the
     hamburger menu on the right. */
  body.incident-focus main.container { padding-left: .45rem; padding-right: .45rem; }
  body.incident-focus:not(.nav-shown) main.container { padding-top: .4rem; }
}

/* bottom sticky summary bar */
.alarm-bottombar { position: sticky; bottom: 0; z-index: 30; display: flex; align-items: stretch;
  margin-top: 1rem; padding: .55rem; background: var(--bs-body-bg); border-top: 2px solid var(--bs-border-color);
  cursor: pointer; }
/* Contents live in an inner box, inset from the bar edges by the bar padding — the flex layout + the
   coordination halo-breathe are on THIS box (owner 2026-08-09), so the glow radiates into the padding,
   staying visible even though the bar itself is edge-to-edge (an outward glow on the bar was clipped
   off-screen). Rounded so the glow curves with the contents. */
.alarm-bottombar .bb-inner { flex: 1 1 auto; min-width: 0; display: flex; gap: .5rem; align-items: stretch;
  border-radius: .5rem; }
/* Mobile: the bar was a bit lost inside the page gutters. Break it out to the full viewport width (flush
   left/right/bottom, no margins) and give it a clearly thicker, stronger top edge (owner 2026-08-09). */
@media (max-width: 768px) {
  .alarm-bottombar { margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); margin-bottom: 0;
    border-radius: 0; border-top: 3px solid var(--bs-secondary-color); padding-left: .7rem; padding-right: .7rem; }
  body.incident-focus main.container { padding-bottom: 0; }   /* let the sticky bar reach the very bottom */
}
/* injected new-alarm banner: full-row, coloured by the incident's state (owner 2026-08-07) */
.alarm-bottombar.bb-new { font-weight: 700; }
.alarm-bottombar.bb-new.state-alarm { background: var(--bs-danger-bg-subtle); border-top-color: var(--bs-danger); }
.alarm-bottombar.bb-new.state-pre_alarm { background: var(--bs-warning-bg-subtle); border-top-color: var(--bs-warning); }
.alarm-bottombar.bb-new .bb-msg { white-space: normal; }
/* New coordination message/action notice: the SAME halo-breathe as the detector/new-alarm pulse, but a
   finite run capped at 8 s (owner 2026-08-08) — four 2 s cycles, then it stops. box-shadow only, so it
   never changes the bar's width (unlike the old scale bounce, which nudged the full-width sticky bar).
   The glow COLOUR now says what arrived (owner 2026-08-25): a message is info, a status action carries
   that action's own tone, a new alarm is danger. It was a theme-neutral body-colour grey, which put the
   page's only new-information signal in its least salient colour — grey lost the pop-out contest
   against every static badge around it. `--bb-flash` is set by the script per event; the fallback keeps
   the old neutral for anything unclassified. */
.alarm-bottombar.bb-coordflash .bb-inner { animation: bb-coordhalo 2s ease-out 4; }
@keyframes bb-coordhalo {
  0%   { box-shadow: 0 0 8px 1px color-mix(in srgb, var(--bb-flash, var(--bs-body-color)) 75%, transparent); }
  55%  { box-shadow: 0 0 11px 9px color-mix(in srgb, var(--bb-flash, var(--bs-body-color)) 0%, transparent); }
  100% { box-shadow: 0 0 8px 0 transparent; }
}
@media (prefers-reduced-motion: reduce) { .alarm-bottombar.bb-coordflash .bb-inner { animation: none; } }

/* ── Unread (owner 2026-08-25) ────────────────────────────────────────────────────────────────────
   The flash decays to nothing in 8 s, so a bar holding a message from three seconds ago looked exactly
   like one holding an hour-old message: look away briefly and the arrival was simply gone. This is the
   residue — a state, not an event. It survives the animation, survives a reload (the last-read message
   id is kept per incident in localStorage) and clears only on evidence that THIS message was read:
   tapping the bar, the message's own timeline line dwelling in the viewport, or you contributing after
   it. Never on scrolling, never on a newer message arriving, never during replay.
   It is a per-viewer reading aid held in the browser — nothing is recorded server-side, precisely so
   nobody can read it as "she has seen it" during an incident. */
.alarm-bottombar.bb-unread { border-top-color: var(--bs-info); }
.alarm-bottombar.bb-unread .bb-inner {
  border-left: .28rem solid var(--bs-info); padding-left: .5rem;
  background: color-mix(in srgb, var(--bs-info) 12%, transparent); }
/* The count rides on the left rail. Singular unread shows a plain dot — a bare "1" is noise, exactly
   the same argument as the log's ack badge. */
.alarm-bottombar .bb-unreadmark { flex: 0 0 auto; align-self: center; margin-right: .1rem;
  min-width: 1.25rem; height: 1.25rem; padding: 0 .3rem; border-radius: .75rem;
  display: none; align-items: center; justify-content: center;
  background: var(--bs-info); color: #06232b; font-size: .72rem; font-weight: 800; }
.alarm-bottombar.bb-unread .bb-unreadmark { display: inline-flex; }
/* Reduced motion gets no flash at all, so lean the static signal harder — this cohort would otherwise
   have had literally no cue that anything arrived. */
@media (prefers-reduced-motion: reduce) {
  .alarm-bottombar.bb-unread .bb-inner { border-left-width: .45rem; }
}
/* Per-DEVICE buzz toggle (owner 2026-08-25). Device-local on purpose: the same person wants a buzz on
   the phone in their pocket and silence on the desktop beside them, which an account-level preference
   cannot express. Dim until hovered so it never competes with the message it sits beside. */
.alarm-bottombar .bb-buzz { flex: 0 0 auto; align-self: center; border: 0; background: none;
  padding: 0 .25rem; font-size: .95rem; line-height: 1; opacity: .35; cursor: pointer; }
.alarm-bottombar .bb-buzz:hover { opacity: .9; }
/* New alarm/pre-alarm arrived on another detector while you're here: the message field radiates the
   shared sonar wave (see `.bb-newalarm::after` above) until you tap through to it (owner 2026-08-08).
   The button's own styling stays put; only the wave signals it. */
.alarm-bottombar .bb-newalarm { flex: 1 1 auto; border: 0; border-radius: .5rem; padding: .4rem .7rem;
  font-weight: 800; color: #fff; background: var(--bs-danger); cursor: pointer; text-align: left; }
/* Pre-alarm (no full alarm in the batch) → orange, not red (owner 2026-08-09). Matches the selector's
   pre-alarm button; dark text for contrast on the amber, as elsewhere. */
.alarm-bottombar .bb-newalarm.pre { background: var(--bs-warning); color: #1a1206; }
.alarm-bottombar .bb-action { display: flex; align-items: center; gap: .4rem; padding: .35rem .7rem; border-radius: .5rem;
  color: #fff; font-weight: 700; flex: 1 1 auto; }
.alarm-bottombar[data-has-msg="1"] .bb-action { flex: 0 0 auto; }         /* share the row with a message */
.alarm-bottombar[data-has-msg="1"] .bb-action .bb-lb { display: none; }   /* compact: emoji only */
.alarm-bottombar .bb-action .bb-ico { font-size: 1.1rem; line-height: 1; }
.alarm-bottombar .bb-action.tone-fire { background: var(--bs-danger); }
.alarm-bottombar .bb-action.tone-brigade { background: var(--bs-warning); color: #1a1206; }
.alarm-bottombar .bb-action.tone-reset { background: var(--bs-success); }
.alarm-bottombar .bb-action.tone-false { background: var(--bs-secondary); }
.alarm-bottombar .bb-msg { flex: 1 1 auto; align-self: center; font-size: .88rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* confirmation popup for the coordination buttons */
.confirm-overlay { position: fixed; inset: 0; z-index: 1050; display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.45); }
.confirm-overlay[hidden] { display: none; }
.confirm-box { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: .7rem;
  padding: 1.1rem 1.2rem; max-width: 22rem; width: calc(100% - 2rem); box-shadow: 0 10px 40px rgba(0,0,0,.3); }
.confirm-box .confirm-msg { font-weight: 600; margin-bottom: .9rem; }
.confirm-box .confirm-actions { display: flex; justify-content: flex-end; gap: .5rem; }

/* Roles: one compact row per role (name · holder · action); both independently claimable, one person
   may hold both. Holder is prominent; de-emphasized action since roles rarely change (owner 2026-08-07). */
.alarm-roles { display: flex; flex-direction: column; gap: .3rem; }
.alarm-roles .role-row { display: flex; align-items: center; gap: .6rem; }
.alarm-roles .role-name { flex: 0 0 auto; min-width: 5.5rem; font-size: .72rem; text-transform: uppercase;
  letter-spacing: .04em; font-weight: 700; color: var(--bs-secondary-color); }
.alarm-roles .role-holder { flex: 1 1 auto; min-width: 0; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alarm-roles .role-row.mine .role-holder { color: var(--bs-success); }
.alarm-roles .role-unclaimed { font-weight: 400; font-style: italic; color: var(--bs-secondary-color); }
.alarm-roles .role-row form { flex: 0 0 auto; margin: 0; }

/* Curated incident events interleaved in the coordination timeline (sounders/resets) — set apart from
   responder messages with an info-tinted rail + italic text (owner 2026-08-07). */
#timeline .coord-event { border-left-color: var(--bs-info) !important; }
#timeline .coord-event-text { color: var(--bs-info-text-emphasis); font-style: italic; }

/* ── Incident playback bar (owner 2026-08-16) ──────────────────────────────────────────────────────
   Fixed to the top while replaying a sealed incident. Dark, so it never reads as part of the page's
   own state — you are looking at the past, and the chrome should say so. Two rows: the slider (with a
   dot per relevant event) then the transport, which is why the buttons fall onto their own line on a
   phone without any media query. */
.pb-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 1055;
  background: #212529; color: #f8f9fa; padding: .4rem .6rem .5rem;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
}
.pb-inner { max-width: 72rem; margin: 0 auto; }
.pb-track-wrap { position: relative; padding-top: .35rem; }
/* The dot strip sits over the range track; it ignores the pointer so the slider stays draggable,
   while each dot re-enables it for its own tooltip + click-to-seek (same trick as the map slider). */
/* Inset by half a thumb width: the range thumb's CENTRE travels from .5rem to width-.5rem (Bootstrap's
   .form-range thumb is 1rem), so a strip spanning the full width puts every dot left of the value it
   marks and the last one past the end (owner 2026-08-16). */
.pb-dots { position: absolute; left: .5rem; right: .5rem; top: .1rem; height: .5rem; pointer-events: none; }
.pb-dot {
  position: absolute; top: 0; width: 7px; height: 7px; margin-left: -3.5px;
  border-radius: 50%; pointer-events: auto; cursor: pointer; border: 1px solid rgba(0, 0, 0, .35);
}
/* The map's condition colours, so a dot reads as the state it produces. */
.pb-dot-alarm { background: #e53935; }
.pb-dot-pre_alarm { background: #fb8c00; }
.pb-dot-reset { background: #43a047; }
.pb-dot-sounder { background: #1e88e5; }
.pb-dot-coord { background: #8e6bd6; }
.pb-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-top: .3rem; }
.pb-speed { width: auto; }
.pb-clock { font-size: .95rem; letter-spacing: .02em; }
.pb-bar .form-range { margin: 0; }
/* Push the page down so the bar never covers the situation card. */
body.pb-on { padding-top: 5.5rem; }
/* Replaying is a read-only reconstruction: the coordination controls stay visible (you can see what
   was available) but cannot be used — a click would write into the present. */
body.replaying #roleBlock form,
body.replaying .alarm-action form { opacity: .5; pointer-events: none; }
/* Placeholder in the bottom bar while replaying a moment before any coordination (owner 2026-08-16). */
.alarm-bottombar .bb-empty { color: var(--bs-secondary-color); font-size: .85rem; font-style: italic; }

/* Detector list "as programmed" switches (owner 2026-08-19): every row carries both the eldur view and
   the programmed one, and the table's class decides which is shown — on screen and in print alike. */
#detList .v-prog, #detList .t-prog { display: none; }
#detList.no-name-override .v-user { display: none; }
#detList.no-name-override .v-prog { display: inline; }
#detList.no-type-override .t-user { display: none; }
#detList.no-type-override .t-prog { display: inline; }
/* "Short type names": the compact Type column the old paper list printed (owner 2026-08-19). */
#detList .ty-short { display: none; }
#detList.short-types .ty-long { display: none; }
#detList.short-types .ty-short { display: inline; }

/* ── Help articles (owner 2026-08-23) ─────────────────────────────────────────────────────────── */
.help-fig { margin-inline: 0; }
.help-fig-body svg { width: 100%; height: auto; }
.help-shot {
  border: 1px dashed var(--bs-border-color);
  border-radius: .375rem;
  background: var(--bs-tertiary-bg);
}
.help-deep {
  border: 1px solid var(--bs-border-color);
  border-radius: .375rem;
  padding: .35rem .6rem;
  background: var(--bs-tertiary-bg);
  max-width: 48rem;
}
.help-deep > summary { cursor: pointer; font-weight: 500; }
.help-deep-body { padding-top: .5rem; }
.help-deep-body > :last-child { margin-bottom: 0; }
.help-fig svg text { fill: currentColor; }
.help-fig svg .dim { opacity: .6; }
.help-chain { max-width: 48rem; }

/* ── Status-legend swatches (help_status_indicators.html, owner 2026-08-24) ───────────────────────
   The help page shows the real thing rather than a screenshot, so the swatches must LOOK like the
   navbar's. The navbar keeps its own copy inline, because the standalone map page includes
   `_navbar.html` without loading this stylesheet — so these values MIRROR `_navbar.html` and must be
   kept in step with it. Change one, change the other. ── */
.slg-bar { display: inline-block; width: .28rem; height: 1.45rem; border-radius: .14rem;
  vertical-align: middle; background: #495057; }
.slg-bar.live { background: #2e7d32; }
.slg-bar.idle { background: #6c757d; }
.slg-bar.warn { background: #f9a825; }
.slg-bar.down { background: #d32f2f; animation: ntpulse 1.2s ease-in-out infinite; }
@keyframes ntpulse { 0%, 100% { opacity: 1 } 50% { opacity: .3 } }
@media (prefers-reduced-motion: reduce) { .slg-bar.down { animation: none } }
.slg-chip { font: 600 .72rem/1.15 ui-monospace, SFMono-Regular, monospace; color: #fff;
  padding: .2rem .32rem; border-radius: .3rem; min-width: 2rem; text-align: center;
  display: inline-block; }
.slg-chip b { font-weight: 700; margin-left: .12rem; }
.slg-chip.a { background: #d32f2f; }
.slg-chip.p { background: #e64a19; }
.slg-chip.t { background: #4fc3f7; color: #000; }
.slg-chip.f { background: #ffb74d; color: #000; }
.slg-chip.d { background: #fdd835; color: #000; }
.slg-chip.z, .slg-chip.z b { background: #495057; color: #adb5bd; }
/* The legend tables carry a swatch column that must not be squeezed by the prose beside it. */
.slg-table td:first-child { white-space: nowrap; width: 1%; padding-right: 1rem; }

/* ── Condition calendar (calendar.html, owner 2026-08-23) ─────────────────────────────────────────
   A month grid whose cells carry two independent signals: a tint = the WORST condition that day
   (inline, the Gantt's class colour at low alpha), and a √-scaled stacked bar = HOW MANY. Desktop
   gives the newest month its own wide card and packs the rest into a grid of small ones; on a phone
   every month is the same size in one scrolling column, newest first (owner). That is why the "big"
   modifier only bites from the md breakpoint up — the mobile layout needs no special case. ── */
.cal-month .card-body { --cal-cell: 3.1rem; }
.cal-monthname { color: inherit; font-weight: 500; font-size: .9rem; }
.cal-monthtotal { font-size: .7rem; white-space: nowrap; }
.cal-dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; margin-left: .3rem; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.cal-wd { font-size: .65rem; text-align: center; line-height: 1.4; }
.cal-day {
  position: relative;
  display: block;
  min-height: var(--cal-cell);
  padding: 1px 2px 5px;
  border-radius: .2rem;
  color: inherit;
  text-decoration: none;
  border: 1px solid transparent;
}
a.cal-day:hover { border-color: var(--bs-border-color); }
/* Data coverage (owner 2026-08-23): green = the panel's daily housekeeping sweep was recorded, so the
   day's emptiness is real. The tint sits UNDER the condition tint — a cell with a condition keeps the
   severity colour as its background and shows coverage on its left edge only, so the two encodings
   never fight for the same pixels. A day we have no sweep for is hatched: unknown, not quiet. */
.cal-day--covered { box-shadow: inset 3px 0 0 var(--bs-success); }
.cal-day--covered:not([style]) { background: rgba(25, 135, 84, .16); }
.cal-day--nodata {
  box-shadow: inset 3px 0 0 var(--bs-secondary-bg);
  background-image: repeating-linear-gradient(
    45deg, transparent, transparent 3px,
    color-mix(in srgb, var(--bs-secondary-color) 22%, transparent) 3px,
    color-mix(in srgb, var(--bs-secondary-color) 22%, transparent) 5px);
}
.cal-day--pad { min-height: 0; }
.cal-day--future { opacity: .35; }
.cal-day--today { border-color: var(--bs-primary); }
.cal-daynum { font-size: .65rem; opacity: .75; }
/* The volume bar: pinned to the cell's bottom edge, its WIDTH √-scaled by the day's total and split
   into class-coloured segments. Bottom-anchored rather than inline so a row of cells reads as a
   miniature bar chart across the week. */
.cal-bar {
  position: absolute; left: 2px; bottom: 2px; height: 3px;
  display: flex; border-radius: 1px; overflow: hidden; min-width: 3px;
}
.cal-bar > span { display: block; }
/* One month per row at every width, every month the same size (owner 2026-08-23) — with the months
   never side by side there is nothing left for a "big" one to be bigger *than*, so the newest month is
   just the first card in the same single column. */
.cal-months { display: grid; gap: .5rem; grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .cal-month .card-body { --cal-cell: 4.2rem; }
  .cal-monthname { font-size: 1rem; }
  .cal-wd { font-size: .75rem; }
  .cal-daynum { font-size: .75rem; }
  .cal-bar { height: 4px; }
}

/* Inline detail rows (D73, `detail-row.js`). The stripe above is positional, so a row injected into a
   table shifts the parity of everything below it. The component stamps the real rows with their true
   parity on insert; these two rules honour that stamp and must therefore come *after* the rule above,
   since they carry the same specificity and win on source order alone. */
.table:not(.table-no-stripe) > tbody > tr.js-stripe-on > * {
  --bs-table-color-type: var(--bs-table-striped-color);
  --bs-table-bg-type: var(--bs-table-striped-bg);
}
.table:not(.table-no-stripe) > tbody > tr.js-stripe-off > * {
  --bs-table-color-type: initial;
  --bs-table-bg-type: initial;
}

/* Per-row event-type exclude on /log (owner 2026-08-24): a ✕ beside every row's event type that drops
   that type from the stream. It sits on EVERY row, so it has to be nearly invisible until the row is
   under the pointer — a column of hard black crosses would read as the log's main feature. Colouring
   it on its own hover says it removes rather than navigates (every other target in the row opens
   something). No hover state on touch, hence the resting opacity is low but never zero. */
.log-xkind { font-size: .8em; opacity: .3; }
tr:hover .log-xkind { opacity: .7; }
.log-xkind:hover, .log-xkind:focus-visible { opacity: 1; color: var(--bs-danger) !important; }


/* Touch tooltips (owner 2026-08-25, tooltips.js). Bootstrap's default 200 px is sized for a word or
   two; eldur's titles are whole sentences ("a timed disconnection's schedule is advisory — the panel
   stores hours, not a date"), and at 200 px on a phone those wrap into a tall grey column. Wider, and
   never wider than the viewport. */
.tooltip {
  --bs-tooltip-max-width: min(22rem, calc(100vw - 2rem));
}
/* A tooltip opened by a tap is dismissed by the next tap — which must not land ON the tooltip and be
   swallowed by it. It is pure output, so it never wants the pointer. */
.tooltip { pointer-events: none; }
