/* UI/UX audit P2, 10 September 2026: no @font-face rule existed, and the CSP
   only allows font-src 'self', so Noto Sans Arabic only ever rendered where
   a client machine happened to have it installed -- a floor machine without
   it silently fell back to Tahoma/Arial at a different type scale. These are
   the actual variable-font files Google Fonts serves for this family
   (v33, one file per unicode range, weight axis 400-700 interpolated by the
   browser -- not four separate static-weight files), split to the two
   ranges this panel actually uses: Arabic script, and basic Latin for the
   digits/punctuation and the <bdi>-isolated Latin tokens (personnel names,
   IDs, coordinates). No CSP change needed: font-src 'self' already covers a
   same-origin file; this only had to be verified, not widened. */
@font-face {
  font-family: "Noto Sans Arabic";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("vendor/fonts/noto-sans-arabic-arabic.woff2") format("woff2");
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891,
    U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
    U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
@font-face {
  font-family: "Noto Sans Arabic";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("vendor/fonts/noto-sans-arabic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}
:root {
  color-scheme: dark;
  --canvas: #071018;
  --surface: #0d1923;
  --raised: #122330;
  --border: #294252;
  --text: #edf5f7;
  --muted: #a9bac3;
  --cyan: #55c7dc;
  --focus: #f6c85f;
  --danger: #ff6b6b;
  --success: #56d39b;
  /* Distinct from --surface/--raised: this is a darker chrome tone specific
     to the topbar and nav rail, and to input/select/textarea fields -- none
     of the existing surface tokens are the semantically correct match, per
     Kimi audit Gate 4, 26 August 2026. */
  --chrome: #08131b;
  --field: #09151e;
  --field-border: #658395;
  /* Alarm-banner backgrounds: a specific danger/focus-tinted dark tone, not
     the generic --raised a card or panel uses. */
  --alarm-bg: #2a1116;
  --alarm-exercise-bg: #211d10;
  --overlay-backdrop: rgba(0, 0, 0, 0.7);
  --tint-subtle: rgba(255, 255, 255, 0.03);
  /* Map symbology doctrine, BLUEPRINT 3.2.0. Mirrors mrkz-api/app/symbology.py
     exactly; the two must be changed together. --danger above is interface
     semantics only and must never colour a friendly map feature. */
  --sym-military: #4a90e2;
  --sym-vital: #56d39b;
  --sym-oil: #b35cd6;
  --sym-hostile: #ff3d3d;
  --sym-threatened-halo: #f6c85f;
  /* A legibility halo, not a map feature: the dark shadow drawn outside a
     symbol so it reads over the raster regardless of the tone beneath it.
     Deliberately NOT part of the --sym-* family, which is three-way pinned to
     symbology.py and CSS/JS by test_symbology_and_renderer.py. */
  --sym-casing: #050c12;
  --space: 16px;
  font-family: "Noto Sans Arabic", Tahoma, Arial, sans-serif;
  font-size: 16px;
}
.data-health {
  margin-block-end: var(--space);
  padding: 8px 16px;
}
.data-health p { margin-block: 4px; }
.action-cell > button { width: 100%; }
* {
  box-sizing: border-box;
}
[hidden] {
  display: none !important;
}
body {
  margin: 0;
  background: var(--canvas);
  color: var(--text);
  line-height: 1.6;
}
button,
input,
select,
textarea {
  font: inherit;
}
button {
  cursor: pointer;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
/* Scoped to the small UI glyphs (nav rail, icon buttons, the auth mark) --
   never bare `svg`. Leaflet's own overlay-pane svg is the vector renderer for
   the grid, every incident marker, and every trajectory polyline: an
   unscoped rule here forces that element's box to this rule's fixed size
   while its viewBox stays full-map, so the whole overlay renders squeezed
   into a 24x24 patch. Investigation MZ-00016, 21 August 2026: this is the
   root cause the map's vector overlay was invisible from Phase 2 onward,
   independent of zoom, viewport, or cache -- confirmed by
   `getBoundingClientRect()` on the live overlay svg. */
.icon {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.auth-screen {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  background: var(--canvas);
}
.auth-card {
  width: min(420px, 100%);
  padding: 32px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
}
.mark {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  color: var(--cyan);
}
.eyebrow {
  margin: 0;
  color: var(--cyan);
  /* 29 September 2026 audit: raised from 0.8rem to the metadata floor. */
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.08em;
}
.auth-card h1,
.view-heading h1 {
  margin: 4px 0 24px;
  font-size: 1.5rem;
}
.auth-card form,
.panel form {
  display: grid;
  gap: 12px;
}
label {
  display: grid;
  gap: 5px;
  color: var(--muted);
  font-size: 0.875rem;
}
input,
select,
textarea {
  width: 100%;
  min-height: 44px;
  padding: 8px 11px;
  border: 1px solid var(--field-border);
  border-radius: 4px;
  background: var(--field);
  font-size: 1rem;
  color: var(--text);
}
textarea {
  resize: vertical;
}
/* Explicit rather than left to the browser default, which is not guaranteed
   to meet contrast against --field. --muted measures 9.2:1 here, well past
   the 4.5:1 minimum. */
input::placeholder,
textarea::placeholder {
  color: var(--muted);
  opacity: 1;
}
.primary,
.secondary,
.text-button,
.icon-button,
.nav-item {
  min-height: 44px;
  border-radius: 4px;
  border: 1px solid transparent;
}
.primary {
  padding: 8px 16px;
  background: var(--cyan);
  color: #041015;
  font-weight: 700;
}
/* UI/UX audit P2, 10 September 2026: the confirm dialog's accept button
   needs a distinct danger treatment for a destructive action -- not the
   ordinary primary-action cyan, which would read as an unremarkable
   confirmation. */
.primary.danger {
  background: var(--danger);
  color: #200404;
}
.primary.danger:active:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 70%, black);
}
.secondary {
  padding: 8px 16px;
  border-color: var(--cyan);
  background: transparent;
  color: var(--cyan);
}
.text-button {
  padding: 6px 10px;
  min-width: 44px;
  background: transparent;
  color: var(--muted);
}
/* UI/UX audit P2, 10 September 2026: "تعديل" and "حذف" sat as equal-weight
   .text-button neighbours in the incident and location rows. This tints
   delete alone with the danger colour so it reads as the higher-consequence
   action, without giving it a different size, position, or a second control
   type that would itself be an inconsistency. */
.text-button.danger-text {
  color: var(--danger);
}
.icon-button {
  display: grid;
  place-items: center;
  width: 44px;
  background: transparent;
  color: var(--muted);
}
/* UI/UX audit P1, 9 September 2026: a filled/active fill for a stateful
   icon button (currently only the mute toggle). --focus, not --danger: this
   is "the alarm tone is off", a configuration the operator chose on
   purpose, not a fault condition -- the same distinction the day-tag badge
   already draws between its calm and loud states. */
.icon-button.active {
  background: color-mix(in srgb, var(--focus) 16%, transparent);
  color: var(--focus);
}
.message {
  min-height: 1.5em;
  margin: 4px 0 0;
  color: var(--focus);
  font-size: 0.875rem;
}
/* 29 September 2026 audit: the freshness line reported a healthy state in the
   same amber --focus tone as alarms and pending states, so the colour carried
   no signal. Status text that is merely informational uses --muted; anything
   genuinely needing attention already switches to .alert/--danger. */
.message.freshness {
  color: var(--muted);
}
/* A standing reading rule, not a transient status line: it carries more weight
   than .message and is anchored by a start-edge rule so it reads as guidance
   attached to the pane. --focus is interface semantics; no --sym-* token may be
   borrowed here, since those belong to map features alone. */
.reading-rule {
  margin: 4px 0 12px;
  padding: 8px 12px;
  border-inline-start: 3px solid var(--focus);
  background: var(--raised);
  color: var(--text);
  font-size: 0.875rem;
  line-height: 1.6;
}
/* A warning that must not be mistaken for a status line. --danger is interface
   semantics and is correct here: this is a fault in the system, not a map
   feature, and no --sym-* token may ever be borrowed for interface chrome. */
.warning-rule {
  margin: 0 0 12px;
  padding: 10px 12px;
  border-inline-start: 3px solid var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, var(--raised));
  color: var(--text);
  font-size: 0.875rem;
  line-height: 1.6;
}
/* Always visible, weight follows what it says (23 August 2026 visibility
   fix). Previously hidden entirely on an operational day so its presence
   alone carried the meaning -- but that made it possible to miss a declared
   exercise/training day too, since noticing it depended on noticing this
   badge was gone. Visibility no longer depends on state; the loud filled
   pill is reserved for a declared exercise/training day, mirroring how the
   escalation badge right beside it changes weight by data-state rather than
   by being hidden. */
.status.day-tag {
  padding: 4px 10px;
  border-radius: 99px;
  border: 1px solid transparent;
  background: var(--focus);
  color: var(--canvas);
  font-weight: 700;
  letter-spacing: 0.02em;
}
.status.day-tag[data-state="عملياتي"] {
  background: transparent;
  border-color: var(--border);
  color: var(--muted);
  font-weight: 500;
}
/* P2-03, 10 September 2026: consolidates what used to be the topbar's
   escalation/day-tag badges, the OPS view-heading's session pill and
   floating day-tag-declare control, and the session panel's own repeated
   meta line into one strip. Sticky directly under the topbar so it stays
   visible while scrolling, and unlike the OPS-only session-state-group it
   replaces, it lives above every view's own section, so it persists across
   view switches rather than being OPS-specific. */
.session-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 10px 24px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  /* --topbar-height is set and kept live by trackTopbarHeight() in app.js
     (ResizeObserver on .topbar) -- 64px is only the pre-JS fallback for the
     one frame before that runs, matching .topbar's own min-height. */
  top: var(--topbar-height, 64px);
  z-index: 400;
}
.session-strip .session-state {
  color: var(--muted);
  font-size: 0.85rem;
}
.session-strip #day-tag-controls {
  margin-inline-start: auto;
}
/* Command disclosure (29 September 2026 audit). Above 480px it is invisible:
   `display: contents` removes the wrapper from the layout so #day-tag-controls
   sits in the strip exactly as it did before this element existed, and the
   summary is hidden. At 480px and below it becomes a real disclosure. */
.command-disclosure {
  display: contents;
}
.command-disclosure > summary {
  display: none;
}
/* 29 September 2026 audit: the strip was 235px tall at 375px because two
   admin-only controls stacked above the map. Both now fold into the account
   menu at that width, and the whole strip collapses to one compact line.
   The controls stay exactly where they are in the DOM at every other width --
   this is a presentation change, not a relocation, so nothing about their
   behaviour or ownership changes. */
@media (max-width: 480px) {
  .session-strip {
    gap: 6px;
    padding: 6px 12px;
  }
  /* The session line's four facts (date, id, hashtag, count) wrap to 3 lines
     here and the two pills repeat what it already says. The session line
     keeps every fact; the pills are the redundant copy, and the escalation
     state stays visible as text inside it. `!important` is not needed: these
     are new rules with no competing declaration at this width. */
  .session-strip .session-state {
    font-size: 0.8125rem;
  }
}
/* State-aligned sources carry a visible mark on the item itself as well as a
   badge, so the label survives a reader skimming headlines. --focus, not
   --danger: this is "read this differently", not "something is wrong". */
.news-item.state-aligned {
  border-inline-start: 3px solid var(--focus);
  padding-inline-start: 12px;
}
.badge.state-aligned-badge {
  background: var(--focus);
  color: var(--canvas);
  font-weight: 700;
}
/* An old or undated item is marked, never hidden. --muted, not --danger: this
   is "read the date", not "something is broken". */
.badge.stale-badge {
  background: var(--raised);
  color: var(--muted);
  border: 1px solid var(--border);
}
.news-heading {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.news-item {
  padding: 12px 0;
  border-block-end: 1px solid var(--border);
}
.news-item:last-child {
  border-block-end: 0;
}
.news-item h3 {
  margin: 0 0 4px;
  font-size: 1rem;
  line-height: 1.5;
}
.news-item .news-source {
  font-size: 0.8125rem;
  color: var(--muted);
}
.news-item .news-source a {
  color: var(--cyan);
}
.news-analysis {
  margin-block-start: 8px;
  padding: 10px 12px;
  border-inline-start: 3px solid var(--cyan);
  background: var(--raised);
  font-size: 0.9375rem;
}
#activity-report {
  max-width: 640px;
  width: calc(100% - 32px);
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  padding: 20px;
}
#activity-report::backdrop {
  background: rgb(7 16 24 / 70%);
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  /* UI/UX audit P2, 10 September 2026: the bare `input, select, textarea`
     rule sets `min-height: 44px; width: 100%` at equal specificity to this
     class, so without these two resets a visually-hidden form control (the
     segmented-control radio) rendered at full touch-target size regardless
     of the explicit 1px width/height above -- a `min-height` is never
     satisfied by a plain `height` declaration alone. */
  min-width: 0;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Lets a keyboard user bypass every marker tab stop on a busy map -- an
   origin/track marker per live incident -- and land directly on the next
   panel instead of tabbing through all of them one at a time. Hidden until
   focused, same pattern as .visually-hidden, but visible while focused so the
   keyboard user can see and use it. */
.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* UI/UX audit P1, 9 September 2026. The link sits inside .map-panel, a
   display:grid box with a fixed aspect-ratio and overflow:hidden -- the
   panel that gives the calibrated raster its shape (Phase 6 step 2, above).
   position:static previously dropped the focused link into that grid's
   normal flow as a full grid item, so a 769x286px box replaced the map for
   as long as the link held focus: the fifth tab stop from the top of the
   page, hit by every keyboard user. Staying absolute -- anchored to
   .map-panel's own position:relative -- keeps the link an overlay near the
   panel's top corner instead, so focusing it can never resize or displace
   the map underneath it. */
.skip-link:focus {
  position: absolute;
  z-index: 600;
  inset-block-start: 8px;
  inset-inline-start: 8px;
  width: auto;
  height: auto;
  margin: 0;
  padding: 8px 12px;
  overflow: visible;
  clip-path: none;
  white-space: normal;
  display: inline-block;
  background: var(--raised);
  color: var(--text);
  border: 1px solid var(--cyan);
  border-radius: 4px;
}
.app {
  min-height: 100vh;
  display: grid;
  /* UI/UX audit P2-06, 10 September 2026: a fixed 64px top row let a long
     centre name or user label overlap the workspace beneath it between 769
     and 1024px -- the ≤768px branch already switched to display:block/auto
     height, and the ≤480px branch already set grid-template-rows: auto 1fr,
     but nothing between those covered 769-1024px. auto here, at every
     width, means the row always reserves exactly what .topbar's own content
     needs; .topbar's min-height (below) keeps the common one-line case at
     the same visual 64px it always was. */
  grid-template: auto 1fr/220px 1fr;
  grid-template-areas: "top top" "rail work";
}
.topbar {
  grid-area: top;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  border-bottom: 1px solid var(--border);
  background: var(--chrome);
  /* 29 September 2026 audit: was `position: sticky`. Because .app is a grid
     container, .topbar's containing block for an absolutely positioned
     descendant was the *grid container*, not the bar -- so the narrow-width
     account menu positioned itself against the whole app and landed on top of
     the session strip. `relative` keeps the bar in normal flow (it is always
     at the top of the app and no scroll container clips it) and gives the menu
     the bar as its containing block, so `top: 100%` still means "just under
     the bar". */
  position: relative;
  z-index: 500;
  /* UI/UX audit P2-06, 10 September 2026: the grid row above is now `auto`
     at every width, so this keeps the common one-line case at the same
     visual 64px it always had, while still letting the row grow past it
     when wrapped content (a long centre name, a long user label) needs
     more -- min-height, not height, for exactly the reason the ≤768px
     branch below already states: a fixed height clips wrapped content. */
  min-height: 64px;
}
.topbar > div,
.top-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
/* UI/UX audit P1, 9 September 2026. Defaults for the topbar account-group
   collapse at 480px and below (the rules that actually change these live in
   that max-width query, further down this file). Declared here, BEFORE any
   media query touches either class, so the cascade order is unambiguous:
   a later unscoped rule at the same specificity would otherwise win over an
   earlier media-query rule regardless of viewport width, which is exactly
   the defect a misplaced copy of this block produced during development --
   the menu never opened because this exact rule, restated after the query,
   silently overrode it at every width. */
.narrow-only {
  display: none;
}
.topbar-account-group {
  display: flex;
  align-items: center;
  gap: 12px;
}
.status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  /* 29 September 2026 audit: raised from 0.8rem to the metadata floor. This
     is the connectivity indicator, not incidental metadata. */
  font-size: 0.85rem;
}
.status.connection::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--danger);
}
.status.connection.online::before {
  background: var(--success);
}
.rail {
  grid-area: rail;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 16px 10px;
  border-left: 1px solid var(--border);
  background: var(--chrome);
  position: sticky;
  /* P2-06, 10 September 2026: was a hardcoded 64px, stale as soon as the
     topbar wraps taller than that (a long centre name or user label between
     769 and 1024px, the exact defect this item fixes elsewhere) -- during a
     scroll the rail would then stick above the topbar's actual bottom edge.
     Same --topbar-height variable .session-strip uses, kept live by
     trackTopbarHeight() in app.js. */
  top: var(--topbar-height, 64px);
  height: calc(100vh - var(--topbar-height, 64px));
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  background: transparent;
  color: var(--muted);
  text-align: right;
}
.nav-item.active {
  background: var(--raised);
  color: var(--text);
  border-color: var(--border);
}
.workspace {
  grid-area: work;
  min-width: 0;
  padding: 24px;
}
.view {
  display: none;
}
.view.active {
  display: block;
}
.view-heading {
  display: flex;
  justify-content: space-between;
  align-items: end;
  margin-bottom: 16px;
}
.view-heading h1 {
  margin-bottom: 0;
}
.panel {
  min-width: 0;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
}
.panel h2 {
  margin: 0 0 12px;
  font-size: 1rem;
}
.panel-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.ops-grid {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
  gap: 16px;
}
.map-panel {
  padding: 0;
  position: relative;
  overflow: hidden;
}
.ops-side {
  display: grid;
  align-content: start;
  gap: 16px;
}
.feed-panel {
  grid-column: 1/-1;
}
.map-readout {
  position: absolute;
  bottom: 12px;
  right: 12px;
  z-index: 410;
  padding: 5px 9px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: rgba(7, 16, 24, 0.9);
  /* 29 September 2026 audit: raised from 0.8rem to the metadata floor. */
  font-size: 0.85rem;
}
.map-panel {
  /* Phase 6 step 2. Two things were wrong at once: the map was a fixed 560px
     tall inside a grid item stretched to the height of the TALLER side column,
     leaving ~300px of dead black below it -- and the panel's shape had nothing
     to do with the raster's, so whatever was left over showed as background.

     The panel now takes the raster's own aspect ratio (--raster-aspect, set
     from the calibration API at map init, so this is not a second copy of the
     image dimensions) and sits at the top of its row. The side column is free
     to be taller; the space beside it is ordinary page background with the feed
     panel below, not a black void inside the map. */
  display: grid;
  aspect-ratio: var(--raster-aspect, 1.432);
  align-self: start;
}
.leaflet-container {
  /* Fills the panel it was just given the right shape for. The single-column
     breakpoints below restate a minimum height, because there the panel height
     is auto and a percentage would collapse to nothing. */
  height: 100%;
  background: var(--canvas);
}
.leaflet-control-zoom a {
  background: var(--surface) !important;
  color: var(--text) !important;
  border-color: var(--border) !important;
}
/* UI/UX audit P2, 10 September 2026: the zoom controls and the skip link are
   plain <a> elements, so the project-wide button/input/select/textarea/
   [tabindex] focus-visible rule above never applied to them and each showed
   the browser's own default outline instead of the standard 3px ring used
   everywhere else on the tab-order path. */
.leaflet-control-zoom a:focus-visible,
.skip-link:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
/* Raster treatment. Root cause of the dead rule, 29 September 2026 audit:
   the previous selector was a bare `.leaflet-tile` -- specificity (0,1,0),
   identical to Leaflet's own `.leaflet-tile { filter: inherit }` in the
   vendored leaflet.css. loadMapAssets() APPENDS leaflet.css to <head> at
   runtime, so it lands AFTER styles.css and won the same-specificity tie:
   the tiles inherited the container's `none` and were never darkened, which
   is why the raster read as a bright paper map inside a dark console.
   `.leaflet-container .leaflet-tile` is (0,2,0) and beats it on specificity
   rather than on source order, so a future change to the load order cannot
   silently kill this again. */
.leaflet-container .leaflet-tile {
  filter: saturate(0.62) brightness(0.52) contrast(1.06);
}
/* Leaflet's vendored default is `background: #ddd` on the container, which
   showed through wherever a tile had not painted yet -- a light grey flash on
   a dark console. Matched at the same (0,2,0) so it, too, wins by
   specificity rather than order. */
.leaflet-container.leaflet-touch,
.leaflet-container.leaflet-container {
  background: var(--canvas);
}
/* Trajectory ETA popup, BRIEF 4 September 2026. Interface chrome: frame and
   text on the chrome tokens only. The --sym-* tokens appear on the category
   glyph alone, inline in app.js exactly as the map draws the site; nothing
   here borrows one for the frame, and nothing in it is ever --sym-hostile.
   "Next up" is stroke weight on the glyph, never a hue. */
.eta-popup .leaflet-popup-content-wrapper {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}
.eta-popup .leaflet-popup-tip {
  background: var(--surface);
  border: 1px solid var(--border);
}
/* Opened below the marker (upper half of the map): the tip moves to the top
   edge and points up at the marker. Placement itself is the popup offset. */
.eta-popup.eta-popup-below .leaflet-popup-tip-container {
  top: -19px;
  transform: scaleY(-1);
}
.eta-popup .leaflet-popup-content {
  margin: 10px 12px;
  direction: rtl;
  text-align: start;
  font-size: 14px;
  line-height: 1.45;
}
.eta-header {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
}
.eta-exercise {
  padding: 1px 8px;
  border: 1px dashed var(--border);
  border-radius: 999px;
  /* 29 September 2026 audit: was 12px, under the 13px metadata floor. */
  font-size: 13px;
  color: var(--muted);
}
.eta-speed,
.eta-caption {
  color: var(--muted);
  font-size: 13px;
}
.eta-caption {
  margin-block: 4px 6px;
  border-block-end: 1px solid var(--border);
  padding-block-end: 4px;
}
.eta-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}
.eta-row {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 8px;
  align-items: start;
  padding: 4px 6px;
  border-radius: 4px;
  background: var(--raised);
}
.eta-row.next {
  border-inline-start: 3px solid var(--border);
}
.eta-glyph {
  margin-block-start: 3px;
}
.eta-row .eta-name {
  grid-column: 2;
}
.eta-row .eta-remaining,
.eta-row .eta-remaining-seconds,
.eta-row .eta-arrival,
.eta-row .eta-none,
.eta-row .eta-likelihood {
  grid-column: 2;
  display: block;
  color: var(--muted);
  font-size: 13px;
}
/* Threat-probability percentage, BRIEF 10 September 2026. Interface chrome
   only, same as every other line in this row -- no --sym-* token, per the
   symbology doctrine's own rule that those belong to the map alone. */
.eta-row .eta-likelihood {
  font-variant-numeric: tabular-nums;
}
.eta-category,
.eta-next {
  color: var(--muted);
  /* 29 September 2026 audit: were 12px, under the 13px metadata floor. */
  font-size: 13px;
}
.eta-next {
  color: var(--muted);
  font-size: 13px;
}
.eta-empty {
  margin: 4px 0;
}
.eta-actions {
  margin-block-start: 8px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--chrome);
  color: var(--text);
  min-height: 44px;
  width: 100%;
}
.grid-line {
  /* 29 September 2026 audit: stroke-opacity 0.54 put the grid at ~2.2:1 over
     the raster's mid-luminance tones. A dark casing under the light line
     restores legibility on every tone without changing the line's own colour
     or its role as an unemphatic reference layer. */
  stroke: #65cde0;
  stroke-opacity: 0.72;
  stroke-width: 1;
  paint-order: stroke;
  filter: drop-shadow(0 0 1px rgba(5, 12, 18, 0.9));
}
/* Leaflet's own control metrics are 30px and 0.75rem (12px). Raised to the
   44px touch minimum and the panel's own type floor (29 September 2026 audit).

   The glyph stays 44px, but the BUTTON's box is kept at its original 30px and
   the extra reach is added as an invisible pseudo-element. Growing the real
   box was measured to break the map: the control sits at the top-left of the
   raster, where a west-edge incident's marker also lands, so a 44px box
   intercepted the marker's clicks -- eta-popup.spec.js's west-edge case timed
   out with "<a.leaflet-control-zoom-in> intercepts pointer events". The
   pseudo-element gives the same 44px touch target without taking any space in
   the layout, so it cannot cover a marker that a 30px control did not already
   cover. */
.leaflet-container .leaflet-control-zoom a {
  position: relative;
  font-size: 1.25rem;
}
.leaflet-container .leaflet-control-zoom a::after {
  content: "";
  position: absolute;
  inset: -7px;
}
/* The map's MARKERS deliberately keep their original size: at this density
   (five symbols inside ~100px, and a turn-point marker sharing its cell with
   its incident marker) any 44px box overlaps its neighbour and steals its
   click -- measured on the deployed panel, 8 of 36 marker pairs intersected
   and the waypoint marker won the hit test over the incident marker beneath
   it. Their legibility is fixed with the dark halo instead. */
.leaflet-container .leaflet-control-attribution {
  font-size: 0.75rem;
}
.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
fieldset {
  margin: 0;
  padding: 0;
  border: 0;
}
legend {
  margin-bottom: 5px;
  color: var(--muted);
  font-size: 0.875rem;
}
.segmented {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}
.segmented label {
  display: block;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
  text-align: center;
  cursor: pointer;
}
.segmented label:has(input:checked) {
  border-color: var(--cyan);
  background: rgba(85, 199, 220, 0.12);
  color: var(--text);
}
/* UI/UX audit P2, 10 September 2026: the visible radio dot inside an
   already-highlighted pill was two affordances for one choice -- the pill's
   own border/background already carries the selected state via :has()
   above. .segmented input reuses .visually-hidden (index.html) rather than
   a second copy of the same clip pattern here: the input stays in the DOM,
   focusable and in the tab order, for a keyboard or screen-reader user. */
.segmented label:has(input:focus-visible) {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
/* form.inline-form / form.filter-bar (tag+class) matches ".panel form"
   (class+tag) on specificity, so the later rule here wins on the normal
   cascade instead of needing !important to force it. .inline-actions is
   used on non-form containers too, where no such conflict exists, but stays
   in this shared rule since the layout itself is identical either way. */
.inline-actions,
form.inline-form,
form.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 8px;
}
.inline-actions > * {
  /* Phase 6 step 2, the day-hashtag control's clipping. `flex: 1` is
     `1 1 0%`: every child is sized from a zero basis and then given an equal
     share, so a select and a button in the same row come out the same width no
     matter what is written in them -- the select's «يوم عملياتي» was cut off and
     the declare button wrapped to three lines. Sizing from content and allowing
     shrink keeps the labels readable, and the row still wraps when it must. */
  flex: 0 1 auto;
}
/* Same defect class, found in the step 2 screenshots rather than reported: the
   speed control's longest option -- «185 — مسيّرة هجومية (افتراضي)» -- was cut
   off inside a half-width form cell. A select cannot ellipsize its own text, so
   a clipped option simply lies about what is selected. It takes the full row. */
#speed-label {
  grid-column: 1 / -1;
}

/* The header band is the one place where a truncated word is a command error
   rather than a cosmetic one: it names what kind of day the floor is having. */
#day-tag-controls > select,
#day-tag-controls > button {
  min-width: max-content;
  white-space: nowrap;
}
.inline-form input,
.inline-form select,
.filter-bar input,
.filter-bar select {
  width: auto;
  flex: 1 1 130px;
}
.session-state,
.badge {
  display: inline-flex;
  padding: 4px 9px;
  border: 1px solid var(--border);
  border-radius: 99px;
  color: var(--muted);
  /* 29 September 2026 audit: 0.8rem (12.8px) was under the design system's
     13px metadata floor; 0.85rem is the sheet's own existing metadata size. */
  font-size: 0.85rem;
}
.badge.warning {
  border-color: var(--focus);
  color: var(--focus);
}
.feed,
.data-list {
  display: grid;
  gap: 6px;
}
.feed-item,
.data-row {
  display: grid;
  /* Trailing `auto` track for .feed-actions (expand/edit/delete), BRIEF 22
     August 2026. .data-row never adds a sixth child, so the empty track
     costs it nothing. */
  grid-template-columns: repeat(5, minmax(0, 1fr)) auto;
  gap: 8px;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
/* Map symbol legibility (29 September 2026 audit). Measured against the
   raster the symbols actually sit on, hostile red read 1.2:1 and the pale
   greens 2.2:1 -- under the 3:1 non-text bar. The fix is the doctrine's own
   halo channel ("stroke weight, halo, dash pattern, icon, or label"), applied
   as a dark drop-shadow rather than an extra SVG element: the glyph's
   geometry, colour and DOM shape are all unchanged, so the specs that assert
   the literal path data and locate the first `circle, path, rect` still hold,
   and no --sym-* hue is touched (those are three-way pinned to symbology.py).
   `drop-shadow` renders outside the mark, so it stays visible on any tone. */
.incident-marker svg,
.track-marker svg,
.waypoint-marker svg {
  filter: drop-shadow(0 0 1.5px var(--sym-casing, #050c12))
          drop-shadow(0 0 1.5px var(--sym-casing, #050c12));
}
/* The friendly-site markers are Leaflet circleMarkers (overlay-pane paths),
   not divIcons, so they need the same halo by class. */
.location-marker,
.location-halo {
  filter: drop-shadow(0 0 1.5px var(--sym-casing, #050c12))
          drop-shadow(0 0 1.5px var(--sym-casing, #050c12));
}
/* display: contents so the wrapper is not a grid box of its own -- its cell
   children become direct grid items of .feed-item's existing five-column spec,
   which is what keeps the header and the data rows aligned. The ARIA row rides
   on the same element (a plain div may carry role="row"; the article may not,
   and axe's aria-allowed-role reports it if it does). 29 September 2026 audit. */
.feed-cell {
  display: contents;
}
.feed-item.exercise {
  border-right: 3px dashed var(--focus);
}
.feed-actions {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
/* Edit/delete for an expanded incident record (Apple-design review, 17
   September 2026); sits at the end of .feed-item-detail. */
.feed-record-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-start;
  padding-top: 4px;
  border-top: 1px solid var(--border);
}
/* Expandable log row detail, BRIEF 22 August 2026 (Part Four). Spans every
   column so it reads as one continuous panel below the row's summary line,
   not a cramped fit into a single grid cell. */
.feed-item-detail {
  grid-column: 1 / -1;
  display: grid;
  gap: 10px;
  padding-block-start: 10px;
  margin-block-start: 4px;
  border-block-start: 1px dashed var(--border);
}
.feed-item-detail h3 {
  margin: 0 0 4px;
  font-size: 0.8125rem;
  color: var(--muted);
}
.feed-item-detail p {
  margin: 2px 0;
  font-size: 0.85rem;
  line-height: 1.5;
  white-space: pre-wrap;
}
.feed-item strong,
.data-row strong {
  color: var(--text);
}
.feed-item span,
.data-row span {
  color: var(--muted);
}
.data-token {
  min-width: 0;
  direction: ltr;
  unicode-bidi: isolate;
  overflow-wrap: anywhere;
  text-align: start;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
/* UI/UX audit P2, 10 September 2026: the audit log's action/result cells pair
   an Arabic label with the raw stored key kept visible as a secondary
   isolated token -- unlike a bare .data-token, this wrapper carries the label
   itself in document order (RTL) and isolates only the nested key. */
.audit-token {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25em;
  min-width: 0;
}
.audit-token .data-token {
  /* Apple-design review, 17 September 2026: opacity dropped -- size plus
     --muted already mark it secondary; a third reduction thinned it to
     5.5:1 at ~13.6px.
     29 September 2026 audit: 0.85em against a 13px parent is 11.05px, the
     smallest text in the product -- below the design system's 13px metadata
     floor and below the sheet's own 0.75rem minimum. Pinned to 13px rather
     than a relative size so it cannot shrink again with its parent. */
  font-size: 13px;
}
/* UI/UX audit P2, 10 September 2026: the news source link was a bare <a>,
   inline by default, so .text-button's min-height/min-width never applied
   and it rendered as a ~40x15px target. This is the first <a class="text-
   button"> in the panel, so the display change is scoped to it rather than
   assumed safe for every future text-button anchor. */
.news-source-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}
.incident-id {
  color: var(--text);
  font-weight: 700;
}
.users-row {
  grid-template-columns: minmax(0, 1fr) minmax(7rem, 0.75fr) auto;
  grid-template-areas:
    "name role action"
    "email status action";
  column-gap: 16px;
  row-gap: 6px;
}
.user-name {
  grid-area: name;
}
.user-role {
  grid-area: role;
}
.user-email {
  grid-area: email;
}
.user-status {
  grid-area: status;
  min-width: 4rem;
  padding-inline-start: 8px;
}
.user-action {
  grid-area: action;
}
.audit-row {
  grid-template-columns:
    minmax(9rem, 1.2fr) minmax(5rem, 0.7fr) minmax(8rem, 1fr)
    minmax(8rem, 1fr) minmax(10rem, 1.2fr);
  column-gap: 16px;
}
/* UI/UX audit P1, 9 September 2026. The context and doctrine version lists
   each add a trailing action/date column past a plain five-column .data-row;
   audit-row's own five tracks were tuned for its own content (a source
   address, a raw action key) and are the wrong shape here -- .version-row
   adds the restore button as a genuine sixth (auto) track instead of forcing
   it into the fifth column, which was P2-11's exact clipping defect at
   1280px. .doctrine-version-row has no action column at all. */
.version-row {
  /* The trailing track is max-content, not bare `auto`: `auto` resolves to
     min-content by default, and a text-button's `white-space: normal` lets
     "استعادة" wrap, so its own min-content width can be shorter than its
     unwrapped text -- exactly P2-11's clip, reproduced by the fix meant to
     remove it. max-content sizes the track to the button's full,
     un-wrapped width instead. */
  /* Minimums dropped to 0 on the four text tracks, 14 September 2026, for the
     same reason as .doctrine-version-row below: 25rem of floors plus gaps and
     the max-content restore button exceed the ~408px column .intel-grid gives
     each panel between 769px and 1183px, and fixed minimums cannot yield. The
     restore button kept its max-content track -- that is P2-11's clipping fix
     and is not being undone -- but the text columns beside it now shrink
     instead of pushing it past the panel's inline-start edge, which is what
     put it 89px outside the viewport at 1024px. */
  grid-template-columns:
    minmax(0, 0.6fr) minmax(0, 1fr) minmax(0, 1fr)
    minmax(0, 1.2fr) max-content;
  column-gap: 16px;
}
.doctrine-version-row {
  /* The track minimums are 0, not 4rem/6rem/6rem/9rem as they were until
     14 September 2026. Those floors sum to 25rem plus 48px of gaps = 448px,
     and between 769px and 1183px .intel-grid is two-up, so this row sits in a
     ~408px column -- narrower than its own minimum. Fixed minimums cannot
     yield, so the row overflowed its panel and pushed the document 89px wider
     than the viewport at 1024px, the horizontal-scroll defect panel.spec.js
     reports. The row's narrow-width fallback below only starts under 768px,
     which never covers that band. The preferred `fr` widths still shape the
     columns at every width where there is room for them. */
  grid-template-columns:
    minmax(0, 0.6fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr);
  column-gap: 16px;
}

/* UI/UX audit P1, 9 September 2026: column headers for every data-list
   (users, locations, audit log, doctrine/context versions). The header row
   is built with the SAME row class its data rows use (audit-row,
   users-row, version-row, doctrine-version-row, or none for the plain
   five-column locations list), so its columns line up with the rows below
   without a second, hand-maintained width spec. Design-system rule: dense
   tables with visible headers at 1024px and up; a labelled-record fallback
   below 768px (the [data-label]::before rule at the bottom of this file),
   never a bare header row squeezed into a narrow width. */
.data-row-head {
  display: grid;
  gap: 8px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  /* Apple-design review, 17 September 2026: header row up from 12px. */
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.intel-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
.placeholder {
  min-height: 220px;
}
.placeholder p,
.backup p {
  color: var(--muted);
  max-width: 72ch;
}
.admin-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
.audit-panel,
.locations-panel {
  grid-column: 1/-1;
}
/* UI/UX audit P2, 10 September 2026: the risk figure was plain wrapped text
   ("خطر 3") inside a five-column .data-row, which is exactly what wrapped to
   a third line in the old two-column layout. A numeric chip, matching the
   existing .badge pill used for the stale/state-aligned news markers. */
.risk-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.8em;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 99px;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  /* 29 September 2026 audit: the chip is a read-only figure, not a control.
     Touch widths give it the panel's minimum target height regardless, since
     it sits in the middle of a row of real controls where a mis-tap is easy. */
}
@media (max-width: 768px) {
  .risk-chip,
  .data-list .text-button,
  .data-list select {
    min-height: 44px;
  }
  .risk-chip {
    min-width: 44px;
  }
}
 /* Apple-design review, 17 September 2026: risk 4-5 was carried by a red
   outline alone -- colour as the only channel, and the same red as the
   delete button two cells over. Now a filled --focus pill (weight channel,
   "operator attention", not "fault"); the digit and the aria-label stay. */
.risk-chip[data-risk="4"],
.risk-chip[data-risk="5"] {
  border-color: var(--focus);
  background: var(--focus);
  color: var(--canvas);
}
.copy-result {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}
.copy-result input {
  flex: 1;
}
.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: 12px;
}
.alarm {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 16px;
  padding: 12px 16px;
  border: 2px solid var(--danger);
  background: var(--alarm-bg);
}
.alarm.exercise {
  border-style: dashed;
  border-color: var(--focus);
  background: var(--alarm-exercise-bg);
}
.alarm div {
  display: grid;
}
.alarm span {
  color: var(--muted);
}
dialog {
  max-width: 520px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
}
dialog::backdrop {
  background: var(--overlay-backdrop);
}
.dialog-actions {
  display: flex;
  justify-content: flex-start;
  gap: 8px;
  margin-top: 16px;
}
@media (max-width: 1024px) {
  .app {
    grid-template-columns: 76px 1fr;
  }
  .rail {
    padding-inline: 8px;
  }
  .nav-item {
    justify-content: center;
  }
  .nav-item span {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .ops-grid {
    grid-template-columns: 1fr;
  }
  .feed-panel {
    grid-column: auto;
  }
  .leaflet-container {
    /* The panel keeps the raster's aspect ratio at every width -- a stated
       height here would put the black bands back on a narrow screen, which is
       the same defect one breakpoint down. A floor stops the map collapsing on
       a very narrow phone; it is below the height the aspect ratio produces at
       every width in the suite, so it never fights it. */
    min-height: 240px;
  }
  .intel-grid {
    grid-template-columns: 1fr 1fr;
  }
  .admin-grid {
    grid-template-columns: 1fr;
  }
}
@media (max-width: 768px) {
  .app {
    display: block;
    padding-bottom: calc(72px + env(safe-area-inset-bottom));
  }
  /* min-height, not height (Kimi audit Gate 4, 26 August 2026): a fixed
     height clipped wrapped topbar content (two lines needed ~78px against a
     ~63px box at 481-768px), overlapping whatever sat below the sticky bar.
     min-height keeps the floor for the common one-line case and lets the
     box grow on the width/content combination that wraps. */
  .topbar {
    min-height: calc(64px + env(safe-area-inset-bottom));
    height: auto;
  }
  .rail {
    position: fixed;
    inset: auto 0 0;
    z-index: 600;
    width: auto;
    height: 64px;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    padding: 5px;
    padding-bottom: calc(5px + env(safe-area-inset-bottom));
    border: 0;
    border-top: 1px solid var(--border);
  }
  .nav-item {
    flex-direction: column;
    gap: 0;
    padding: 3px;
  }
  .nav-item span {
    position: static;
    inline-size: auto;
    block-size: auto;
    clip-path: none;
    /* Apple-design review, 17 September 2026: 0.7rem (11.2px) sat at the
       mobile legibility floor for a non-bold custom face; 0.75rem is the
       smallest size used anywhere else in this sheet. */
    font-size: 0.75rem;
  }
  .workspace {
    padding: 16px;
  }
  .intel-grid {
    grid-template-columns: 1fr;
  }
  .leaflet-container {
    min-height: 240px;
  }
  .feed-item,
  .data-row {
    grid-template-columns: 1fr 1fr;
  }
  .users-row {
    grid-template-columns: minmax(0, 1fr) minmax(7rem, 0.75fr) auto;
  }
  .audit-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  .version-row,
  .doctrine-version-row {
    grid-template-columns: 1fr 1fr;
  }
  .view-heading {
    align-items: start;
  }
  /* UI/UX audit P1, 9 September 2026. Design-system rule: dense tables with
     a visible header at 1024px and up, a labelled-record fallback below
     768px -- never a bare header row squeezed into a width too narrow for
     it, and never a data cell with no label at all. The header row itself is
     redundant once every cell carries its own label, so it is dropped
     rather than also being made to fit a narrower grid. */
  .data-row-head {
    display: none;
  }
  .data-row [data-label] {
    display: grid;
    gap: 2px;
  }
  .data-row [data-label]::before {
    content: attr(data-label);
    color: var(--muted);
    /* 29 September 2026 audit: 0.7rem (11.2px) sat below the sheet's own
       0.75rem floor and the design system's 13px metadata minimum, and these
       labels carry the ONLY column identity in the narrow layout -- the least
       appropriate place to shrink type. */
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
  }
  /* A <button>'s own three-line wrap under a `::before` label read worse
     than the plain prefix every text cell gets, but a button IS a real
     element and can host generated content, unlike a <select>. */
  button[data-label]::before {
    content: attr(data-label);
    display: block;
    color: var(--muted);
    /* 29 September 2026 audit: raised off 0.7rem, same reason as above. */
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
  }
  /* A <select> cannot host ::before at all (a replaced form control), so its
     label is a real sibling element instead (labelledControl() in app.js) --
     shown only here, since the header row above 1024px already names the
     column. */
  .data-list-control {
    display: grid;
    gap: 2px;
  }
  .data-list-control-label {
    color: var(--muted);
    /* 29 September 2026 audit: raised off 0.7rem, same reason as above. */
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.02em;
  }
}
@media (min-width: 768.02px) {
  .data-list-control-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}
@media (max-width: 480px) {
  .topbar {
    padding: 0 10px;
  }
  .workspace {
    padding: 10px;
  }
  .ops-grid {
    gap: 10px;
  }
  .panel {
    padding: 12px;
  }
  .map-panel {
    padding: 0;
  }
  /* UI/UX audit P1, 9 September 2026. OPS.md documents a 320px mobile map
     floor; the aspect-ratio rule above (Phase 6 step 2) was rendering the
     map at roughly 256px at 390px width, below that floor, because a
     narrow viewport times the raster's own width:height ratio is
     necessarily short. Below 480px the panel drops the aspect-ratio
     constraint and takes an explicit height instead -- the raster no longer
     fills the panel's width exactly, but the map's own fitBounds/zoomSnap:0
     logic (initializeMap(), app.js) and the ResizeObserver already wired to
     the container both already re-fit to whatever shape the container ends
     up being, so nothing here needs to know the raster's own dimensions. */
  .map-panel {
    aspect-ratio: unset;
    height: 320px;
  }
  .leaflet-container {
    height: 100%;
    min-height: 320px;
  }
  .form-row {
    grid-template-columns: 1fr;
  }
  .feed-item,
  .data-row {
    grid-template-columns: 1fr;
  }
  .users-row {
    grid-template-columns: 1fr;
    grid-template-areas: "name" "role" "email" "status" "action";
  }
  .alarm {
    align-items: stretch;
    flex-direction: column;
  }
  .auth-card {
    padding: 24px 18px;
  }
  /* UI/UX audit P3, 10 September 2026: `.inline-form > *` (specificity
     0,0,1,0) needed !important only because `.inline-form input,
     .inline-form select` (0,0,1,1) elsewhere in this file outranks it on
     specificity alone regardless of source order. Naming the actual tags
     here raises this rule to the same specificity and lets normal cascade
     order (this block comes later in the file) decide it instead. */
  .inline-form > input,
  .inline-form > select,
  .inline-form > button {
    flex-basis: 100%;
  }
}
/* Pointer-only feedback keeps mouse and trackpad cues off touch devices.
   Press feedback remains available to every input type. */
@media (hover: hover) and (pointer: fine) {
  .primary.danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--danger) 85%, white);
  }
  .text-button.danger-text:hover:not(:disabled) {
    background: color-mix(in srgb, var(--danger) 16%, transparent);
    color: var(--danger);
  }
  .primary:hover:not(:disabled) {
    background: color-mix(in srgb, var(--cyan) 85%, white);
  }
  .secondary:hover:not(:disabled),
  .icon-button:hover:not(:disabled) {
    background: var(--raised);
    color: var(--text);
  }
  .text-button:hover:not(:disabled) {
    background: var(--raised);
    color: var(--text);
  }
  .nav-item:hover:not(.active) {
    background: var(--tint-subtle);
    color: var(--text);
  }
  .section-nav a:hover,
  #doctrine-reading summary:hover { color: var(--cyan); }
}
.primary:active:not(:disabled) {
  background: color-mix(in srgb, var(--cyan) 80%, black);
}
.secondary:active:not(:disabled),
.icon-button:active:not(:disabled) {
  background: color-mix(in srgb, var(--raised) 70%, black);
}
.text-button:active:not(:disabled) {
  background: color-mix(in srgb, var(--raised) 70%, black);
}
.nav-item:active:not(.active) {
  background: var(--raised);
}
@media (prefers-reduced-motion: no-preference) {
  button,
  .nav-item,
  input,
  select,
  textarea {
    transition:
      color 150ms,
      background-color 150ms,
      border-color 150ms,
      opacity 150ms;
  }
}
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation: none !important;
    transition: none !important;
  }
}

/* Escalation state badge. UI/UX audit P2, 10 September 2026: previously
   plain inline text indistinguishable from ordinary muted metadata -- a
   static icon (markup, see #escalation-state in index.html) plus a pill
   treatment matching .status.day-tag right beside it, with visual weight
   increasing by level (outline -> filled outline -> filled background),
   while text still carries the meaning on its own per the design system's
   "never communicate status by colour alone" rule. */
.status.escalation {
  padding: 4px 10px;
  border-radius: 99px;
  border: 1px solid var(--border);
  color: var(--muted);
}
.status.escalation svg {
  width: 14px;
  height: 14px;
  flex: none;
}
.status.escalation[data-state="تأهب"] {
  border-color: var(--focus);
  color: var(--focus);
  font-weight: 700;
}
.status.escalation[data-state="طارئة"] {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
  font-weight: 700;
}
#context-body {
  width: 100%;
  font-family: inherit;
  background: var(--canvas);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 12px;
  line-height: 1.6;
}

/* The escalation badge added a third item to the top bar, which overflowed at
   375px and produced horizontal page scroll. The badge must stay visible --
   the standing condition is exactly what an operator needs at a glance -- so
   the bar wraps and its items shrink rather than the badge being hidden. */
.topbar {
  flex-wrap: wrap;
  row-gap: 4px;
}
.topbar > div,
.top-actions {
  min-width: 0;
  flex-wrap: wrap;
}
.status {
  min-width: 0;
}
@media (max-width: 480px) {
  .app {
    grid-template-rows: auto 1fr;
  }
  .topbar {
    padding: 6px 12px;
  }
  /* The escalation badge and the day-tag pill both stay directly visible at
     this width: mobile-chrome.spec.js asserts it, and both carry a fact an
     operator reads at a glance. The compaction here comes from the command
     disclosure below, not from hiding status. */
  /* The command disclosure becomes a real disclosure here, and it is the
     only thing in the strip that can collapse: the session line, the
     escalation badge and the day tag stay visible unconditionally. */
  .command-disclosure {
    display: block;
    inline-size: 100%;
    margin-inline-start: 0;
  }
  .command-disclosure > summary {
    display: block;
    min-height: 44px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--chrome);
    color: var(--text);
    cursor: pointer;
    font-size: 0.85rem;
  }
  .command-disclosure[open] > summary {
    margin-block-end: 6px;
  }
  .command-disclosure #day-tag-controls {
    margin-inline-start: 0;
    padding-top: 4px;
  }
  .status.escalation {
    font-size: 0.75rem;
  }
  /* UI/UX audit P1, 9 September 2026. At this width the account group
     (user label, activity report, logout) is inside the collapsed menu
     below, not inline, so the ellipsis truncation that used to fight the
     bar's own wrapping is no longer needed -- the full name reads fine
     inside the menu panel, which has the whole viewport width to work
     with. */
  #user-label {
    white-space: normal;
  }
  /* .narrow-only currently names one element, #topbar-menu-toggle, which is
     itself an .icon-button (display: grid) -- restoring that display value
     here rather than picking a new one keeps its own centring rule intact. */
  .narrow-only {
    display: grid;
  }
  /* The account group becomes a floating panel anchored under the "⋮"
     toggle instead of three more items competing for the topbar's one row
     -- the defect this replaces: the topbar wrapped to three lines at this
     width because the account group and the status group were both trying
     to occupy the same row's remaining space. */
  /* Closed by default at this width (no [hidden] attribute involved: that
     would also hide the group above 480px, where it is the visible default
     part of the bar, not a menu at all -- .open is a width-scoped state
     toggled by app.js, never fighting the wide layout's own visibility). */
  .topbar-account-group {
    display: none;
  }
  .topbar-account-group.open {
    display: flex;
    position: absolute;
    inset-inline-end: 8px;
    top: calc(100% + 4px);
    z-index: 700;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    min-width: 180px;
    /* 29 September 2026 audit: the panel is positioned against .topbar (which
       is `position: relative` for exactly this reason) and capped to the space
       actually below the bar, so it scrolls internally instead of growing down
       over the session strip and the escalation badge. `dvh` keeps that cap
       honest when a mobile URL bar is showing. */
    max-block-size: calc(100dvh - var(--topbar-height, 64px) - 16px);
    overflow-y: auto;
    padding: 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--raised);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  }
  .topbar-account-group #user-label {
    padding: 6px 10px;
    color: var(--muted);
    font-size: 0.85rem;
  }
  .topbar-account-group .text-button {
    justify-content: flex-start;
    text-align: start;
  }
}
/* --- The chat assistant, BLUEPRINT section 10, Phase 6 step 3 -------------
   Lives in `.ops-side`, so none of this touches `.map-panel`'s aspect ratio.
   The log is a bounded scroller rather than a growing column: an unbounded
   transcript pushes the session panel below it off the screen, and on a phone
   it would push the whole side column past the fold. */
.assistant-panel {
  display: grid;
  gap: 10px;
}
.assistant-notice {
  margin: 0;
}
.assistant-log {
  display: grid;
  gap: 8px;
  align-content: start;
  max-block-size: 320px;
  min-block-size: 96px;
  overflow-y: auto;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
}
.assistant-turn {
  display: grid;
  gap: 3px;
  padding: 7px 9px;
  border-radius: 4px;
  background: var(--tint-subtle);
}
.assistant-user {
  border-inline-start: 3px solid var(--border);
}
.assistant-assistant {
  /* --cyan, the interface accent. Deliberately NOT --sym-military: the
     symbology tokens carry doctrinal meaning on the map and borrowing one for
     a chat bubble is how a colour quietly acquires a second meaning. */
  border-inline-start: 3px solid var(--cyan);
}
/* A sentinel is not an answer, and must not be able to look like one at a
   glance. Same treatment as the other machine-analysis warnings in the panel. */
.assistant-sentinel {
  border-inline-start: 3px solid var(--focus);
  background: rgba(246, 200, 95, 0.09);
}
.assistant-role {
  margin: 0;
  font-size: 0.75rem;
  opacity: 0.72;
}
.assistant-text {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.assistant-refs {
  margin: 0;
  /* Apple-design review, 17 September 2026: 0.78rem at 0.8 opacity stacked
     two reductions on 12.5px text; one size step up, no fade. */
  font-size: 0.8125rem;
  color: var(--muted);
}
#assistant-form {
  display: grid;
  gap: 8px;
}

/* Backup freshness panel, BLUEPRINT section 12, Phase 6 step 7. */
.backup-facts {
  display: grid;
  gap: 4px;
  margin-block: 8px;
}
.backup-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
}
.backup-label {
  color: var(--muted);
}
.backup-value {
  font-variant-numeric: tabular-nums;
}
.badge.ok {
  border-color: var(--success);
  color: var(--success);
}
.message.alert {
  color: var(--danger);
}

/* September audit: explicit form labels and constrained intrinsic widths. */
.inline-form > label,
.filter-bar > label {
  flex: 1 1 130px;
  min-inline-size: 0;
}
.inline-form label > input,
.inline-form label > select,
.filter-bar label > input,
.filter-bar label > select {
  inline-size: 100%;
  min-inline-size: 0;
  max-inline-size: 100%;
}
.intel-grid > * { min-inline-size: 0; }
.section-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-block-end: 16px;
}
.section-nav a,
#auth-recovery { color: var(--muted); }
.section-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 12px;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 4px;
}
#doctrine-reading summary {
  min-height: 44px;
  padding: 10px 12px;
  cursor: pointer;
}
.section-nav a[aria-current="location"] {
  border-color: var(--border);
  background: var(--raised);
  color: var(--text);
}
.section-nav a:focus-visible,
#doctrine-reading summary:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}
.intel-grid > article,
.admin-grid > section {
  scroll-margin-top: calc(var(--topbar-height, 64px) + var(--session-strip-height, 160px) + 16px);
}
@media (max-width: 768px) {
  .view { --section-nav-height: 53px; }
  .section-nav {
    position: sticky;
    top: calc(var(--topbar-height, 64px) + var(--session-strip-height, 160px));
    z-index: 350;
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    padding-block: 4px;
    border-block-end: 1px solid var(--border);
    background: var(--surface);
    scrollbar-width: thin;
    scrollbar-color: var(--border) var(--surface);
  }
  .section-nav::-webkit-scrollbar { height: 4px; }
  .section-nav::-webkit-scrollbar-track { background: var(--surface); }
  .section-nav::-webkit-scrollbar-thumb {
    border-radius: 99px;
    background: var(--border);
  }
  .section-nav a {
    flex: 0 0 auto;
    white-space: nowrap;
  }
  .intel-grid > article,
  .admin-grid > section {
    scroll-margin-top: calc(var(--topbar-height, 64px) + var(--session-strip-height, 160px) + var(--section-nav-height) + 16px);
  }
}
#doctrine-reading summary { margin-block-end: 12px; }
@media (min-width: 1025px) {
  .ops-grid { grid-template-rows: auto 1fr; align-items: start; }
  .map-panel { grid-column: 1; grid-row: 1; }
  .ops-side { grid-column: 2; grid-row: 1 / 3; }
  .feed-panel { grid-column: 1; grid-row: 2; }
}
