/* =============================================================================
 * ui.css — the NOC design system.  "INSTRUMENT"
 *
 * WHAT CHANGED AND WHY (rebuild, 2026-07)
 * ---------------------------------------
 * The previous system was competent but anonymous: indigo #4f46e5 on translucent
 * blur-cards with 14 px radii and Tailwind's stock palette (red-500, orange-500,
 * amber-500, blue-500, emerald-500, slate-500 — literally). Every one of those is
 * a default someone else chose, and together they read as a template rather than
 * as a product. Blanket `backdrop-filter` on every card made it worse: frosted
 * glass everywhere is decoration that costs GPU time on a screen that is on for
 * twelve hours and never says anything about the data underneath.
 *
 * This system is built for what the app actually is: a dispatch console. Its rules:
 *
 *   1. COLOUR IS MEANING, NEVER DECORATION. The interface is structurally
 *      achromatic — ink, hairlines, plate surfaces. Hue is spent only on priority,
 *      unit status and alarm. One signature hue (phosphor teal) is reserved for
 *      interaction: focus, selection, "you are here". Because nothing else is
 *      coloured, a P I dispatch is visible from across the room.
 *   2. SURFACES ARE PLATES, NOT GLASS. Opaque layers (--s0…--s3) separated by
 *      hairlines, with a 1 px top light-catch. Blur survives in exactly one place —
 *      chrome and overlays, where translucency communicates "floating above".
 *   3. DATA IS SET IN MONO. Every timestamp, count, id, channel and measurement is
 *      IBM Plex Mono with tabular figures, so columns of numbers line up and the
 *      same table looks identical on every machine in the room.
 *   4. THREE TYPOGRAPHIC VOICES. Archivo (wide, signage) for titles and figures,
 *      Inter for prose and controls, Plex Mono for data and micro-caps labels.
 *   5. TECHNICAL-DRAWING DETAIL. Corner ticks, hairline rules, a 32 px measure
 *      grid, micro-caps section labels. The polish comes from precision, not from
 *      gradients and shadows.
 *
 * COMPATIBILITY: every token name and every class name from the previous system
 * still exists — --glass / --glass-2 are now aliases onto the plate scale, so the
 * ~20 pages that still carry local CSS inherit the new look without being touched.
 * Page-specific CSS is folded in here progressively; anything still inline in a
 * page file is a leftover, not a pattern to copy.
 *
 * Cache-busted by ?v=<filemtime>, served immutable for a year — see
 * docker/caddy/Caddyfile.
 * ========================================================================== */

/* ================================ fonts =================================
 * Three faces, all OFL, self-hosted, subset to the Slovenian UI's codepoints by
 * tools/build_fonts.py (1902 KB -> 226 KB). Output is committed; nothing builds at
 * deploy time.
 *
 * NO ?v= ON THESE URLS. layout.php's <link rel="preload"> must match the url()
 * below byte-for-byte or the browser treats them as two resources and downloads
 * the face twice. The filename IS the version: to ship a changed face, rename it
 * (they are served immutable for a year — see the Caddyfile).
 *
 * The text and display faces are VARIABLE, which is what makes the intermediate
 * weights below (450, 520, 560, 640, 680) render as authored instead of snapping
 * to the nearest of four static cuts — the flaw the old four-file setup had.
 * ===================================================================== */
@font-face{
  font-family:"InterVar";
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url("fonts/Inter-var.subset.woff2") format("woff2-variations"),
      url("fonts/Inter-var.subset.woff2") format("woff2");
}
@font-face{
  /* Archivo carries a width axis (62-125%). Set wide it is control-room signage;
     that width is the single most recognisable thing about this interface. */
  font-family:"ArchivoVar";
  font-style:normal;
  font-weight:100 900;
  font-stretch:62% 125%;
  font-display:swap;
  src:url("fonts/Archivo-var.subset.woff2") format("woff2-variations"),
      url("fonts/Archivo-var.subset.woff2") format("woff2");
}
@font-face{ font-family:"PlexMono"; font-style:normal; font-weight:400; font-display:swap;
  src:url("fonts/PlexMono-400.subset.woff2") format("woff2"); }
@font-face{ font-family:"PlexMono"; font-style:normal; font-weight:500; font-display:swap;
  src:url("fonts/PlexMono-500.subset.woff2") format("woff2"); }
@font-face{ font-family:"PlexMono"; font-style:normal; font-weight:600; font-display:swap;
  src:url("fonts/PlexMono-600.subset.woff2") format("woff2"); }

/* ============================ design tokens ============================
 * Three blocks, deliberately duplicated: default (light), [data-theme="dark"], and
 * prefers-color-scheme for visitors who never touched the toggle. `light-dark()`
 * would collapse this to one block, but an unsupported browser resolves it to
 * *nothing* and the console renders unstyled. A dispatch screen does not get to
 * fail that way, so the duplication stays.
 * ===================================================================== */
:root{
  color-scheme: light dark;

  --display:"ArchivoVar", "Archivo", ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  --font:"InterVar", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono:"PlexMono", ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;

  /* ---- geometry: one small scale, used everywhere -------------------- */
  --r1:6px;            /* chips, ticks, small controls */
  --r2:9px;            /* buttons, inputs */
  --r3:12px;           /* plates, cards, panels */
  --rail-w:236px;      /* navigation rail, expanded */
  --rail-mini:62px;    /* navigation rail, icons only */
  --top-h:52px;        /* status strip */
  --measure:1440px;    /* content column */

  /* ---- LIGHT: "daylight". Cool paper, not white; plates lift off it. -- */
  --s0:#eaeef2;        /* page */
  --s1:#ffffff;        /* plate */
  --s2:#f5f8fa;        /* raised: inputs, popovers, table head */
  --s3:#e9eef3;        /* inset / hover */
  --fg:#0a0e13; --dim:#535e6c; --faint:#8894a2;
  --stroke:rgba(10,14,19,.11); --stroke-2:rgba(10,14,19,.055); --hi:rgba(10,14,19,.035);
  --light-catch:rgba(255,255,255,.9);   /* 1 px top highlight on plates */
  --chrome:rgba(255,255,255,.78);       /* the ONLY translucent surfaces */
  --tick:rgba(10,14,19,.28);            /* technical-drawing corner ticks */
  /* Fallback <select> chevron. A background image cannot inherit ink, so the
     stroke is baked per theme — %23535e6c is --dim. */
  --chev:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23535e6c' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");

  /* Signature hue: phosphor teal. Not in the priority palette, so it can never be
     mistaken for a severity — it means "interactive / selected / you are here". */
  --accent:#00867a; --accent-2:#0aa596; --accent-ink:#ffffff;
  --accent-soft:color-mix(in srgb, var(--accent) 11%, transparent);

  --shadow-sm:0 1px 1.5px rgba(10,14,19,.055);
  --shadow:0 1px 2px rgba(10,14,19,.05), 0 8px 24px -14px rgba(10,14,19,.20);
  --shadow-lg:0 2px 6px -2px rgba(10,14,19,.09), 0 22px 60px -22px rgba(10,14,19,.26);

  /* ---- signals. Hand-mixed at matched lightness so no single one shouts
     louder than its severity warrants, and so the set reads as one paint box
     rather than six unrelated defaults. --p70/--warn are deliberately dark in
     this theme: amber on paper is otherwise unreadable. */
  --ok:#0a8257; --bad:#c81f36; --warn:#94620a; --info:#1465cf;
  --p90:#c81f36;   /* P I  kritična */
  --p80:#c25a09;   /* P I  nujna    */
  --p70:#a06c00;   /* P II          */
  --p60:#1465cf;   /* P III         */
  --p50:#0a8257;   /* razrešeno / prosto */
  --pna:#67727f;   /* brez prioritete */

  /* categorical series — a SEPARATE order from priority so a chart hue never reads
     as a priority colour. Validated (dataviz skill): light mode, surface #f6f7f9 —
     CVD worst ΔE 9.1 (protan), normal-vision floor 19.6. The sub-3:1 contrast-vs-
     surface WARN is discharged by always pairing a series with a visible label or a
     table view (both present on analitika.php). Do not reorder — adjacency was
     checked. Carried over from the previous system unchanged: the validation is
     still valid and re-mixing them would invalidate it. */
  --cat-1:#2a78d6; --cat-2:#eb6834; --cat-3:#1baf7a; --cat-4:#eda100; --cat-5:#e87ba4;

  --blur:16px; --sat:120%;
  --grid:rgba(10,14,19,.075); --grid-2:rgba(10,14,19,.11);

  /* THE FIELD — the surface the console sits on. See the block comment at "THE
     FIELD" below for what each layer does; these are the per-theme strengths, so
     the whole surface can be tuned (or muted) without touching one piece of
     geometry.

     No --topo* tokens any more: the contour field was retired from the console (see
     "RETIRED LAYERS"). login.php defines its own, so the front door is unaffected. */
  /* THE LIGHT IS WEAK ON PAPER, AND THAT IS THE POINT. The first attempt ran the
     rake at 75 % white, reasoning that a highlight on a pale surface has to
     out-white the surface. What it actually did was bleach the panel to flat white
     and erase the grooves — the only thing that made it read as metal. On paper the
     SHADOW does the modelling: a light touch of warm white at the lamp end, a
     firmer cool falloff at the other, and the engraving left alone to be seen. */
  --rake-c:#fffaf2;  --rake:.28;        /* the raking light, upper-left */
  --sheen-c:#ffffff; --sheen:.18;       /* the anisotropic band across the grain */
  --fill-c:#5a78a0;  --fill:.07;        /* cool counter-light, lower-right */
  /* A vignette on paper must be a DEEPER TINT, not a grey: black over a cool
     paper desaturates it to putty and the page looks dusty at the edges. */
  --vig-c:#26384d;
  --vig:.17;                            /* edge falloff, over every layer */

  /* aliases — the previous system's surface names, kept so untouched page CSS
     inherits the new plates instead of breaking. --glass is now OPAQUE, which is
     what neutralises the stray `backdrop-filter` still sitting in a few pages. */
  --bg:var(--s0); --bg2:var(--s2);
  --glass:var(--s1); --glass-2:var(--s2);
}

:root[data-theme="dark"]{
  /* ---- DARK: "ink". The primary theme — this is what runs on the wall. ---- */
  --s0:#05070a; --s1:#0b0f14; --s2:#111721; --s3:#19212c;
  --fg:#e7edf4; --dim:#93a0b1; --faint:#5f6c7c;
  --stroke:rgba(255,255,255,.075); --stroke-2:rgba(255,255,255,.04); --hi:rgba(255,255,255,.045);
  --light-catch:rgba(255,255,255,.055);
  --chrome:rgba(11,15,20,.72);
  --tick:rgba(255,255,255,.24);
  --chev:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2393a0b1' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");

  --accent:#1ed3b8; --accent-2:#5cecd6; --accent-ink:#03231f;
  --accent-soft:color-mix(in srgb, var(--accent) 14%, transparent);

  --shadow-sm:0 1px 2px rgba(0,0,0,.5);
  --shadow:0 1px 2px rgba(0,0,0,.55), 0 10px 30px -16px rgba(0,0,0,.85);
  --shadow-lg:0 8px 24px -10px rgba(0,0,0,.6), 0 30px 80px -30px rgba(0,0,0,.95);

  /* On ink the signals go bright and slightly desaturated — full-chroma red on
     near-black vibrates and is genuinely harder to read at a glance. */
  --ok:#3ed898; --bad:#ff5c6b; --warn:#ffb43d; --info:#5aa5ff;
  --p90:#ff5062; --p80:#ff8b3d; --p70:#ffc247; --p60:#4d9dff; --p50:#3ed898; --pna:#7c8899;

  /* categorical series — dark, validated against surface #080a0f: L 0.48–0.67,
     chroma ≥0.1, CVD worst ΔE 8.4 (protan), all ≥3:1 vs surface. Same order/roles
     as light so a series keeps its hue-identity across the theme toggle. */
  --cat-1:#3987e5; --cat-2:#d95926; --cat-3:#199e70; --cat-4:#c98500; --cat-5:#d55181;

  --grid:rgba(255,255,255,.062); --grid-2:rgba(255,255,255,.10);

  /* THE FIELD, on ink — the theme the surface is designed for. Two deliberate
     choices survive from the machined panel it replaces:

     The raking light is WARM and the counter-light is COOL. That temperature split
     is the whole reason it reads as light falling on a surface rather than as a
     grey gradient: in the physical world light is warm and shadow is cool, and the
     eye knows it without being told.

     Nothing here animates. On a screen that stays open for a twelve-hour shift the
     only thing that should move is a dispatch arriving. (The sign-in page does
     drift its blooms — it is looked at for fifteen seconds, not twelve hours.) */
  --rake-c:#fff4e4;  --rake:.17;        /* warm */
  --sheen-c:#ffffff; --sheen:.055;
  --fill-c:#8cb4ff;  --fill:.045;       /* cool */
  --vig-c:#000;
  /* Tuned DOWN from .58 after looking at it: at that strength the falloff ate the
     raking light it is supposed to shape, and the panel went back to being flat
     black with a grid on it. A vignette has to leave something to fall off from. */
  --vig:.44;
}

@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --s0:#05070a; --s1:#0b0f14; --s2:#111721; --s3:#19212c;
    --fg:#e7edf4; --dim:#93a0b1; --faint:#5f6c7c;
    --stroke:rgba(255,255,255,.075); --stroke-2:rgba(255,255,255,.04); --hi:rgba(255,255,255,.045);
    --light-catch:rgba(255,255,255,.055);
    --chrome:rgba(11,15,20,.72);
    --tick:rgba(255,255,255,.24);
    --chev:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2393a0b1' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    --accent:#1ed3b8; --accent-2:#5cecd6; --accent-ink:#03231f;
    --accent-soft:color-mix(in srgb, var(--accent) 14%, transparent);
    --shadow-sm:0 1px 2px rgba(0,0,0,.5);
    --shadow:0 1px 2px rgba(0,0,0,.55), 0 10px 30px -16px rgba(0,0,0,.85);
    --shadow-lg:0 8px 24px -10px rgba(0,0,0,.6), 0 30px 80px -30px rgba(0,0,0,.95);
    --ok:#3ed898; --bad:#ff5c6b; --warn:#ffb43d; --info:#5aa5ff;
    --p90:#ff5062; --p80:#ff8b3d; --p70:#ffc247; --p60:#4d9dff; --p50:#3ed898; --pna:#7c8899;
    --cat-1:#3987e5; --cat-2:#d95926; --cat-3:#199e70; --cat-4:#c98500; --cat-5:#d55181;
    --grid:rgba(255,255,255,.062); --grid-2:rgba(255,255,255,.10);
    /* Mirrors :root[data-theme="dark"] — see the notes there. */
    --rake-c:#fff4e4; --rake:.17; --sheen-c:#ffffff; --sheen:.055;
    --fill-c:#8cb4ff; --fill:.045; --vig-c:#000; --vig:.44;
  }
}

/* ============================== baseline ============================== */
*{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; }

/* =============================================================================
 * THE PANEL — the surface the console is mounted on.
 *
 * WHY IT WAS REBUILT (2026-07, second time)
 * -----------------------------------------
 * The previous background was six simultaneous effects: three aurora blobs
 * drifting on long periods, a radar beam rotating over a constellation of nodes
 * that ignited and emitted rings as it passed, a topographic contour texture, a
 * cursor-tracked pool of accent light, grain and a vignette. Every layer was
 * individually justified in a comment. The sum was a sci-fi HUD — aurora plus
 * radar plus constellation plus cursor-glow is the visual language of a crypto
 * dashboard, and it made a serious dispatch console look like a template with the
 * effects slider pushed to maximum.
 *
 * The failure was one of KIND, not of degree: adding a seventh layer, or dimming
 * all six, would not have fixed it. Restraint is what reads as designed.
 *
 * The replacement was a MACHINED PANEL — anodized aluminium, an engraved 16/96 px
 * graticule, one raking light. It fixed the noise problem and introduced a quieter
 * one: an edge-to-edge straight ruling behind everything is the pattern that always
 * reads as filler, and it made the product look like a wireframe of itself.
 *
 * So the ruling is gone and the console now sits on a CONTOUR FIELD. Three layers,
 * no animation, no colour:
 *
 *   svg.topo          the map: generated contours + hypsometric bands
 *   .bgfield .rake    ONE light source, upper-left, plus the anisotropic sheen
 *   .bgfield .vig     edge falloff, over everything
 *   body::before      grain (kept: it is what stops large ink flats from banding)
 *
 * THE THREE IDEAS THAT DO THE WORK
 *
 *   1. IT IS A MAP, IN A PRODUCT WHOSE MAIN VIEW IS A MAP. Contour lines are this
 *      product's own vocabulary rather than a texture borrowed from a template —
 *      and unlike a grid, no two lines are the same, so the eye reads it as a
 *      surface with a shape instead of as a repeating tile it must look past.
 *      Generated by tools/build_topo.py into assets/topo.svg; the header of that
 *      file carries the geometry rationale.
 *
 *   2. WARM LIGHT, COOL SHADOW. The rake is warm (#fff4e4) and the counter-light
 *      is cool (#8cb4ff), both at a few percent. Physical light is warm and
 *      shadow is cool; matching that is what makes the eye accept a gradient as
 *      illumination rather than see it as a grey smear.
 *
 *   3. THE LIGHT FALLS ON THE MAP. .rake follows the map in DOM order, so it paints
 *      over it: the contours are brightest where the lamp hits and all but vanish
 *      in the far corners. Carried over from the panel, where it was the best idea
 *      in the file.
 *
 * IT IS INLINE GEOMETRY, NOT A MASK OR A BACKGROUND IMAGE, and that is a resolution
 * decision — both of those rasterise once at the layer's size and resample from the
 * bitmap thereafter, which no hairline survives. See tools/build_topo.py.
 *
 * COST: one composited paint of one fixed element, plus ~34 static paths the vector
 * rasteriser draws once. Nothing animates, nothing is hit-tested, nothing repaints
 * on scroll. The old field ran a 30 fps canvas and a pointermove listener for twelve
 * hours a shift; this runs nothing.
 *
 * CONTRAST: as before, everything is behind .shell (z-index:1) on plate surfaces
 * that are OPAQUE, so no amount of light back here can cost a dispatch a single
 * point of contrast.
 * ========================================================================== */
body{
  font-family:var(--font); margin:0; color:var(--fg); font-size:13.5px; line-height:1.5;
  letter-spacing:-0.006em; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  font-optical-sizing:auto;
  background:var(--s0); min-height:100vh;
}

/* The field itself. Fixed and clipped, so the surface stays put while the content
 * scrolls over it — a mounted surface does not scroll — and so none of the layers
 * inside can ever produce a scrollbar.
 *
 * Its own background is now just the wash that gives the field a sky and a floor;
 * the map is the inline <svg> child. A fixed element paints once and is composited
 * thereafter, where `background-attachment:fixed` on body would repaint the whole
 * background on every scroll frame — same picture, none of the cost. */
.bgfield{
  position:fixed; inset:0; z-index:0; pointer-events:none; overflow:hidden;
  contain:layout paint style;
  background-image:
    radial-gradient(140% 92% at 50% -18%,
      color-mix(in srgb, var(--s2) 62%, transparent), transparent 66%);
}

/* ---- THE MAP IS GONE FROM THE APP SHELL. It lives only on login.php now, which
 * carries its own copy of these four rules and its own (louder) tokens — see the
 * "RETIRED LAYERS" note further down for why it came off the console. */

/* ---- THE LIGHT. One source, and one specular band.
 *
 * The source sits mostly OFF the top-left corner: what you see is its falloff,
 * not the hot spot. A highlight whose centre is on screen reads as a spotlight
 * aimed at the page; a highlight whose centre is off screen reads as a room lit
 * from a window, which is what this wants.
 *
 * The second layer is the anisotropic sheen — the broad, soft band that brushed
 * metal throws back at you across the direction of its grain. It is set at a
 * shallow angle to the horizontal brushing so it crosses the grain rather than
 * running with it.
 *
 * The third is the cool counter-light in the opposite corner. Without it the
 * unlit half of the panel is simply darker; with it, that half is darker AND
 * cooler, and the surface acquires a front and a back. */
.bgfield .rake{
  position:absolute; inset:0;
  background-image:
    /* THE CORE. A tighter, hotter pool just off the corner. Two nested falloffs
       rather than one is what separates "a lamp is up there" from "the top of the
       page is a bit lighter": a real source has a bright near-field AND a long
       tail, and the eye reads the pair as distance. */
    radial-gradient(46% 36% at 4% -14%,
      color-mix(in srgb, var(--rake-c) calc(var(--rake) * 62%), transparent),
      transparent 72%),
    /* THE WASH. The long tail, carried well across the panel. */
    radial-gradient(132% 102% at 10% -8%,
      color-mix(in srgb, var(--rake-c) calc(var(--rake) * 100%), transparent),
      transparent 76%),
    /* THE SHEEN. The broad band brushed metal throws back across its grain — set
       at a shallow angle to the horizontal brushing so it crosses it. */
    linear-gradient(104deg,
      transparent 12%,
      color-mix(in srgb, var(--sheen-c) calc(var(--sheen) * 100%), transparent) 40%,
      transparent 70%),
    /* THE COUNTER-LIGHT, cool, in the corner the lamp does not reach. */
    radial-gradient(74% 62% at 104% 110%,
      color-mix(in srgb, var(--fill-c) calc(var(--fill) * 100%), transparent),
      transparent 72%);
}

/* ---- THE FALLOFF. Deliberately the topmost layer rather than part of the panel's
 * own background, because it has to darken the LIGHT as well as the metal. Without
 * it the illumination runs into all four corners, every part of the frame is
 * equally bright, and the composition has no centre.
 *
 * TWO LAYERS, AND THE FIRST ONE IS THE POINT. A symmetric vignette was the first
 * attempt and it fought the lamp: light coming from the upper left wants its shadow
 * in the LOWER RIGHT, and a falloff that darkens all four corners equally cancels
 * exactly the gradient it should be reinforcing. So the first layer is diagonal,
 * running away from the source at the same angle the sheen crosses — that is what
 * gives the panel a lit end and a dark end. The second is the ordinary edge
 * darkening that seats the whole frame.
 *
 * Black on ink, where falloff really is an absence of light; a deep blue-grey on
 * paper, where black would desaturate the cool paper to putty. */
.bgfield .vig{
  position:absolute; inset:0;
  background-image:
    linear-gradient(118deg,
      transparent 20%,
      color-mix(in srgb, var(--vig-c) calc(var(--vig) * 72%), transparent) 98%),
    radial-gradient(132% 108% at 46% -2%,
      transparent 38%,
      color-mix(in srgb, var(--vig-c) calc(var(--vig) * 100%), transparent) 100%);
}

/* Grain. 2 % of an SVG noise field, composited once as a fixed layer — it is what
 * stops the large flat ink areas from banding on cheap wall panels, and on the
 * panel it doubles as the tooth of a bead-blasted finish. Cheap: one paint, never
 * repainted on scroll, never hit-tested. */
body::before{
  content:""; position:fixed; inset:0; z-index:0; pointer-events:none;
  opacity:.5; mix-blend-mode:soft-light;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.045'/%3E%3C/svg%3E");
}
@media (prefers-reduced-transparency: reduce){ body::before{ display:none; } }

/* RETIRED LAYERS, kept as no-ops so a cached page or a fragment that still carries
 * the old markup paints nothing instead of a stray blob or wedge on top of the
 * panel. The elements are gone from layout.php and login.php; assets/field.js,
 * which drove the radar and the cursor pool, is deleted.
 *
 * .bgfield .tk held the registration crosses, which belonged to the graticule and
 * mean nothing over a contour field: a crosshair marks the intersection of two
 * straight rules, and there are no longer any straight rules to intersect.
 *
 * body::after held an earlier topographic contour MASK — a low-contrast texture
 * pretending to be information, rasterised through a mask. Its successor was real
 * geometry (assets/topo.svg, inlined by layout.php) and it has now been retired from
 * the console too, for a reason that only shows up in the assembled page rather than
 * in the layer on its own: CONTOURS ARE CLOSED LOOPS. Over a full-bleed field they
 * read as terrain, but the console is a column of opaque cards, so all the eye gets
 * is the fragments visible in the gaps AROUND those cards — arcs and rings with no
 * landform to belong to, which reads as debris from an older design. It stays on
 * login.php, where the page IS one plate on an open field and the map is the
 * composition. The lesson is worth keeping: a background pattern is only as good as
 * the shape of the holes it is seen through.
 *
 * svg.topo is therefore no longer styled here (login.php has its own copy), and the
 * --topo / --topo-i / --topo-band tokens are gone from the app themes with it.
 *
 * There is deliberately NO `.bgfield svg.topo{display:none}` no-op in the list below,
 * unlike the other retired layers: that selector outranks login.php's own `svg.topo`
 * rule and would blank the front door's map. It is not needed anyway — asset_url()
 * versions the stylesheet by mtime, so HTML old enough to still inline the contour
 * field is served with the old CSS that styles it. */
body::after{ content:none; }
.sweep, .bgfield .au, .bgfield .spot, .bgfield canvas, .bgfield .tk{ display:none; }

a{ color:var(--accent); text-decoration:none; }
a:hover{ color:var(--accent-2); }
::selection{ background:color-mix(in srgb,var(--accent) 30%,transparent); }

/* One focus treatment for the whole app: a ring in the signature hue, drawn
 * OUTSIDE the element so it never shifts layout. Text controls opt out — they
 * already state focus with their own border + halo, and both at once reads as an
 * error state rather than as "your cursor is here". */
:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; border-radius:var(--r1); }
input:focus-visible, select:focus-visible, textarea:focus-visible{ outline:none; }

/* scrollbars — thin, and only visible while there is something to scroll */
*{ scrollbar-width:thin; scrollbar-color:color-mix(in srgb,var(--dim) 35%,transparent) transparent; }
*::-webkit-scrollbar{ width:11px; height:11px; }
*::-webkit-scrollbar-thumb{ background:color-mix(in srgb,var(--dim) 32%,transparent); border-radius:20px;
  border:3.5px solid transparent; background-clip:content-box; }
*::-webkit-scrollbar-thumb:hover{ background:color-mix(in srgb,var(--dim) 55%,transparent); background-clip:content-box; }

/* ========================== chrome: shell + rail =======================
 * The old chrome put 18 destinations in a horizontally scrolling strip, which
 * means half of them were always off-screen and the current page frequently was
 * too. A rail shows every destination at once, grouped by what the operator is
 * doing (operativa / promet / arhiv / varstvo / sistem), and gives the app a
 * vertical spine to hang the content column on.
 *
 * Three widths: full (--rail-w), icons-only ([data-rail="mini"], remembered in
 * localStorage), and off-canvas drawer under 900 px.
 * ===================================================================== */
.shell{ display:flex; align-items:flex-start; min-height:100vh; position:relative; z-index:1; }

.rail{
  position:sticky; top:0; z-index:70; flex:0 0 var(--rail-w); width:var(--rail-w);
  height:100vh; display:flex; flex-direction:column;
  /* A whisper of the signature hue at the top of the spine, over the chrome
     translucency. Gives the rail its own light source instead of reading as a
     flat grey gutter — the only decoration on the whole surface. */
  background:
    linear-gradient(180deg, color-mix(in srgb,var(--accent) 6%,transparent), transparent 38%),
    var(--chrome);
  backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  border-right:1px solid var(--stroke);
  transition:flex-basis .22s cubic-bezier(.2,.7,.3,1), width .22s cubic-bezier(.2,.7,.3,1);
}
@media (prefers-reduced-motion: reduce){ .rail{ transition:none; } }
:root[data-rail="mini"]{ --rail-w:var(--rail-mini); }

/* brand block — the one place the display face is set at signage width.
 *
 * A LINK, not a decorative div. The logo is the first thing anybody clicks to get
 * back to the dashboard, and it used to do nothing at all.
 *
 * Both child classes are prefixed `wm-`. `.nm` and `.sb` are generic names owned by
 * five other components, and the source-badge `.sb` rule (declared ~1000 lines
 * later, so it wins every property the scoped rule does not name) was leaking
 * padding:3px 8px and a 7 px radius into the subtitle — which is why the second
 * line sat indented from the first instead of flush under it. Scoped names rather
 * than an !important arms race.
 */
.rail .head{
  display:flex; align-items:center; gap:10px; padding:0 14px; height:var(--top-h);
  border-bottom:1px solid var(--stroke); flex:0 0 auto;
  color:inherit; text-decoration:none;
  /* The same 1 px milled lip the plates carry, so the brand reads as the top face
     of the spine instead of a header taped above it. */
  box-shadow:0 1px 0 var(--light-catch);
}
.rail .head:focus-visible{ outline:2px solid var(--accent); outline-offset:-3px; border-radius:4px; }
.rail .head .logo{
  width:30px; height:30px; border-radius:8px; object-fit:cover; display:block; flex:0 0 auto;
  /* A physical chip: hairline edge, top light-catch, and a shadow it sits on. The
     old 3 px accent halo read as a focus ring around something not focusable. */
  box-shadow:inset 0 0 0 1px var(--light-catch), 0 0 0 1px var(--stroke),
             0 1px 2px color-mix(in srgb,var(--fg) 20%,transparent);
  transition:box-shadow .16s ease;
}
.rail .head:hover .logo{
  box-shadow:inset 0 0 0 1px var(--light-catch),
             0 0 0 1px color-mix(in srgb,var(--accent) 45%,var(--stroke)),
             0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);
}
.rail .head .wordmark{ min-width:0; overflow:hidden; }
.rail .head .wm-nm{
  font-family:var(--display); font-weight:680; font-stretch:112%; font-size:16px;
  letter-spacing:-0.008em; line-height:1.02; white-space:nowrap; display:block;
  color:var(--fg); transition:color .16s ease;
}
.rail .head:hover .wm-nm{ color:var(--accent); }
.rail .head .wm-sub{
  /* 9 px fits "NADZORNO OPERATIVNI CENTER" — 27 mono characters, ≈159 px — inside
     the 236 px rail with about 9 px to spare. The tracking cannot grow past .055em
     without clipping, so ellipsis stays as the backstop for a longer APP_SUBTITLE. */
  font-family:var(--mono); font-size:9px; font-weight:500; color:var(--dim);
  letter-spacing:.055em; text-transform:uppercase; white-space:nowrap; display:block;
  margin-top:2px; overflow:hidden; text-overflow:ellipsis;
}
@media (prefers-reduced-motion: reduce){
  .rail .head .logo, .rail .head .wm-nm{ transition:none; }
}

/* The scroller. Nineteen destinations do not fit under about 1200 px of viewport,
 * and a hard-cut list reads as TRUNCATED rather than scrollable — the single
 * biggest reason the rail looked unfinished. The mask fades whichever edge still
 * has content beyond it; --fade-t/--fade-b are driven by noc.js from scrollTop and
 * are both 0 px when everything fits, which collapses the gradient to a hard stop
 * and costs nothing. */
.rail .groups{
  flex:1 1 auto; overflow-y:auto; overflow-x:hidden; overscroll-behavior:contain;
  padding:12px 10px 14px;
  --fade-t:0px; --fade-b:0px;
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 var(--fade-t),
                                     #000 calc(100% - var(--fade-b)), transparent 100%);
          mask-image:linear-gradient(to bottom, transparent 0, #000 var(--fade-t),
                                     #000 calc(100% - var(--fade-b)), transparent 100%);
  scrollbar-width:thin; scrollbar-color:transparent transparent;
}
/* The scrollbar appears on approach rather than sitting there permanently. */
.rail:hover .groups{ scrollbar-color:color-mix(in srgb,var(--fg) 18%,transparent) transparent; }
.rail .groups::-webkit-scrollbar{ width:6px; }
.rail .groups::-webkit-scrollbar-track{ background:transparent; }
.rail .groups::-webkit-scrollbar-thumb{ background:transparent; border-radius:3px; }
.rail:hover .groups::-webkit-scrollbar-thumb{ background:color-mix(in srgb,var(--fg) 18%,transparent); }
.rail .groups::-webkit-scrollbar-thumb:hover{ background:color-mix(in srgb,var(--fg) 30%,transparent); }

.rail .navgrp{
  font-family:var(--mono); font-size:9.5px; font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--faint); padding:0 8px; margin:17px 0 5px;
  display:flex; align-items:center; gap:9px; white-space:nowrap; user-select:none;
}
/* Fades out rather than running the full width — a rule that stops on its own
   looks drawn, one that hits the edge looks like a table border. */
.rail .navgrp::after{ content:""; flex:1; height:1px;
  background:linear-gradient(90deg, var(--stroke), transparent); }
.rail .groups > .navgrp:first-child{ margin-top:1px; }

.rail a.it{
  position:relative; display:flex; align-items:center; gap:11px; padding:7.5px 9px; margin:1px 0;
  border-radius:var(--r2); color:var(--dim); font-size:13px; font-weight:520;
  letter-spacing:-0.004em; white-space:nowrap;
  transition:background-color .14s ease, color .14s ease;
}
/* Three weights of icon — faint at rest, dim on hover, accent when current — so
   the eye can find the active row without reading a single label. */
.rail a.it svg{ width:17px; height:17px; flex:0 0 auto; stroke-width:1.7;
  color:var(--faint); transition:color .14s ease; }
.rail a.it .lb{ overflow:hidden; text-overflow:ellipsis; }
.rail a.it:hover{ color:var(--fg); background:var(--hi); }
.rail a.it:hover svg{ color:var(--dim); }
/* Inset ring: the default +2px offset would be clipped by the scroller's edge. */
.rail a.it:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
.rail a.it.active{
  color:var(--fg); font-weight:600;
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--accent) 15%,transparent),
    color-mix(in srgb,var(--accent) 4%,transparent) 70%, transparent);
}
/* The "you are here" mark: a 3 px paint stroke on the spine, not a filled pill. */
.rail a.it.active::before{
  content:""; position:absolute; left:-10px; top:50%; transform:translateY(-50%);
  width:3px; height:20px; border-radius:0 3px 3px 0; background:var(--accent);
  box-shadow:0 0 12px color-mix(in srgb,var(--accent) 60%,transparent);
}
.rail a.it.active svg{ color:var(--accent); }

.rail .foot{ flex:0 0 auto; border-top:1px solid var(--stroke); padding:10px; display:flex; gap:8px;
  align-items:center; }
.rail .who{ min-width:0; flex:1; display:flex; align-items:center; gap:9px; }
.rail .who .av{
  width:27px; height:27px; border-radius:50%; flex:0 0 auto; display:grid; place-items:center;
  font-family:var(--mono); font-size:11px; font-weight:600; color:var(--accent);
  background:var(--accent-soft); box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent);
}
.rail .who .wtx{ min-width:0; display:flex; flex-direction:column; gap:1px; }
.rail .who .nm2{ font-size:12.5px; font-weight:560; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rail .who .rl{ font-family:var(--mono); font-size:9.5px; color:var(--faint); letter-spacing:.08em;
  text-transform:uppercase; }

/* rail collapse toggle — sits at the bottom, out of the way of the destinations */
.rail .collapse{
  flex:0 0 auto; width:29px; height:29px; display:grid; place-items:center; border-radius:var(--r1);
  border:1px solid var(--stroke); background:var(--s2); color:var(--dim); cursor:pointer; transition:.14s ease;
}
.rail .collapse:hover{ color:var(--accent); border-color:color-mix(in srgb,var(--accent) 40%,var(--stroke)); }
.rail .collapse svg{ width:15px; height:15px; transition:transform .18s ease; }
:root[data-rail="mini"] .rail .collapse svg{ transform:rotate(180deg); }

/* ---- mini state: icons only, labels become floating flags ---- */
:root[data-rail="mini"] .rail .head{ padding:0; justify-content:center; }
:root[data-rail="mini"] .rail .head .wordmark,
:root[data-rail="mini"] .rail a.it .lb,
:root[data-rail="mini"] .rail .who .wtx,
:root[data-rail="mini"] .rail .navgrp::after{ display:none; }
:root[data-rail="mini"] .rail .navgrp{ justify-content:center; padding:0; margin:15px 0 6px; gap:0; font-size:0; }
:root[data-rail="mini"] .rail .navgrp::before{
  content:""; width:16px; height:1px; border-radius:1px; background:var(--stroke); }
:root[data-rail="mini"] .rail .groups{ padding-left:8px; padding-right:8px; }
:root[data-rail="mini"] .rail a.it{ justify-content:center; padding:9px 0; }
/* No room for the gradient wash at 62 px — the icon and the spine carry it. */
:root[data-rail="mini"] .rail a.it.active{
  background:color-mix(in srgb,var(--accent) 13%,transparent); }
:root[data-rail="mini"] .rail a.it.active::before{ left:-8px; height:22px; }
:root[data-rail="mini"] .rail .foot{ flex-direction:column; }

/* Collapsed-rail label flag.
 *
 * Rendered on <body> by noc.js, NOT as an ::after inside .rail .groups. That
 * scroller sets overflow-y:auto, and CSS forces the OTHER axis to auto too when
 * one axis is non-visible — so the old flag was clipped to the 62 px column and
 * never appeared at all. It only ever widened the scroller's content to 178 px.
 * Verified in Chrome: overflow-x computed 'auto', scrollWidth 178 vs client 61.
 *
 * Shown on focus as well as hover, so a collapsed rail stays keyboard-navigable —
 * previously there was no way to tell the icons apart without a mouse. */
.railtip{
  position:fixed; z-index:200; left:0; top:0; padding:6px 10px;
  border-radius:var(--r1); background:var(--fg); color:var(--s1);
  font-size:12px; font-weight:600; letter-spacing:-.003em; white-space:nowrap;
  box-shadow:var(--shadow-lg); pointer-events:none;
  opacity:0; transform:translate(-4px,-50%);
  transition:opacity .12s ease, transform .12s ease;
}
.railtip.on{ opacity:1; transform:translate(0,-50%); }
.railtip::before{
  content:""; position:absolute; right:100%; top:50%; transform:translateY(-50%);
  border:5px solid transparent; border-right-color:var(--fg);
}
@media (prefers-reduced-motion: reduce){ .railtip{ transition:none; } }

/* ---- the content column ---- */
.view{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; }

/* Status strip. Everything on it answers "is what I am looking at current?" —
 * feed state, clock, then account. It is NOT a navigation surface any more. */
header.top{
  position:sticky; top:0; z-index:60; display:flex; align-items:center; gap:14px;
  height:var(--top-h); padding:0 clamp(14px,2.4vw,26px); flex:0 0 auto;
  background:var(--chrome); backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  border-bottom:1px solid var(--stroke);
  transition:box-shadow .18s ease, border-color .18s ease;
}
/* Set by noc.js once the page is scrolled. A sticky bar over moving content has
   to say it is floating — without this the strip and the content read as one
   flat plane and the first row of the table looks like it belongs to the bar. */
header.top.lifted{
  border-bottom-color:color-mix(in srgb,var(--fg) 13%,transparent);
  /* Mixed from --fg, not a hardcoded black: the light theme's ink is near-black
     but its surfaces are not, and a fixed rgba shadow reads as grime on paper. */
  box-shadow:0 10px 26px -22px color-mix(in srgb,var(--fg) 80%,transparent);
}
@media (prefers-reduced-motion: reduce){ header.top{ transition:none; } }
header.top .sp{ flex:1; }

/* page identity, restated in the strip so a screenshot of the console says where
   it was taken (the rail scrolls and collapses to icons; the strip does neither) */
header.top .where{ display:flex; align-items:baseline; gap:8px; min-width:0; }
header.top .where .t{
  font-family:var(--display); font-weight:600; font-stretch:108%; font-size:13.5px;
  letter-spacing:.002em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* The section, in the same micro-caps mono the rail uses for its group headings —
   so the crumb and the rail heading are visibly the same label. */
header.top .where .crumb{
  font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:.1em;
  text-transform:uppercase; color:var(--faint); white-space:nowrap; flex:0 0 auto;
}
header.top .where .slash{ color:var(--faint); font-weight:400; flex:0 0 auto; }
/* Hairline between the state cluster and the action cluster. */
header.top .vr{ width:1px; height:19px; flex:0 0 auto; background:var(--stroke); margin:0 1px; }
@media (max-width:640px){
  header.top .where .crumb, header.top .where .slash, header.top .vr{ display:none; }
}

/* the drawer trigger, only on narrow screens */
.railbtn{ display:none; }

/* live indicator: a lamp, not a badge. Green pulse = ingest is current. */
.lamp{
  display:inline-flex; align-items:center; gap:8px; padding:4px 10px 4px 8px; border-radius:999px;
  border:1px solid var(--stroke); background:var(--s2);
  font-family:var(--mono); font-size:10.5px; font-weight:600; letter-spacing:.09em;
  text-transform:uppercase; color:var(--dim); white-space:nowrap;
}
.lamp .led{ width:7px; height:7px; border-radius:50%; flex:0 0 auto; background:var(--ok);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--ok) 20%,transparent), 0 0 10px color-mix(in srgb,var(--ok) 70%,transparent);
  animation:lamp 2.6s ease-in-out infinite; }
.lamp.warn .led{ background:var(--warn);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--warn) 20%,transparent), 0 0 10px color-mix(in srgb,var(--warn) 70%,transparent); }
.lamp.bad .led{ background:var(--bad);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--bad) 22%,transparent), 0 0 12px color-mix(in srgb,var(--bad) 80%,transparent); }
.lamp.warn{ color:var(--warn); } .lamp.bad{ color:var(--bad); }
@keyframes lamp{ 50%{ opacity:.45; } }
@media (prefers-reduced-motion: reduce){ .lamp .led{ animation:none; } }
@media (max-width:820px){ .lamp .tx{ display:none; } .lamp{ padding:5px; } }

/* ---- source strip: one plate per external feed ------------------------
 * `.lamp` above answers "is OUR ingest current" and there is exactly one of it,
 * owned by health.php. A page that reads several EXTERNAL feeds (DARS events and
 * roadworks, GURS, CAA, ARSO …) has a different question per feed, so the strip
 * lives here rather than being re-invented per page — every freshness affordance
 * in the app is then one component with one vocabulary.
 *
 * Two ages, on purpose: `.age` is when WE reached the upstream, `.pub` is when the
 * SOURCE says it published. A feed that answers instantly with a forty-minute-old
 * document is exactly the failure a single "updated" line hides.
 *
 * The LED reuses `.lamp .led`'s 7 px geometry but is deliberately STATIC — a row of
 * six pulsing dots is a disco, and the pulse means "live stream" on the lamp. Here
 * the state word and the plate carry the meaning.
 *
 * Every state is distinguishable WITHOUT reading its word: hue (via --c), border
 * style (dashed = cache), and LED fill (hollow ring = cache) all differ. Colour is
 * never the only channel — see design.php.
 *
 * `.sc.cache` = serving a last-good body because the upstream is unreachable. The
 * app emitted `stale:true` for months (mapfeeds.php's gurs/caa_uav passthrough) with
 * nothing able to render it; "old but true" is not "broken" and must not read as it.
 *
 * EVERY selector is scoped under `.srcstrip`, including the state variants. `.sc` is
 * already taken — arso.php:319 uses `.fi .sc` for the fire-danger score figure — and
 * a bare `.sc.warn` in a shared stylesheet would eventually repaint it from across
 * the app. Same rule the `.segbar`-not-`.seg` note below states. */
.srcstrip{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:0 0 14px; }
.srcstrip .sc{
  --c:var(--dim);
  display:inline-flex; align-items:center; gap:8px; padding:6px 11px; min-width:0;
  border:1px solid color-mix(in srgb,var(--c) 30%,var(--stroke)); border-radius:var(--r2);
  background:var(--s2); font-size:11.5px; color:var(--dim); white-space:nowrap;
}
.srcstrip .sc .nm{
  font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:.1em;
  text-transform:uppercase; color:var(--fg);
}
.srcstrip .sc .led{
  width:7px; height:7px; border-radius:50%; flex:0 0 auto; background:var(--c);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--c) 18%,transparent);
}
.srcstrip .sc .st{
  font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; color:var(--c);
}
.srcstrip .sc .age{ font-variant-numeric:tabular-nums; }
.srcstrip .sc .pub{ font-variant-numeric:tabular-nums; color:var(--faint); }
.srcstrip .sc .n{ font-family:var(--mono); font-weight:600; font-variant-numeric:tabular-nums;
  color:var(--fg); }
/* Mid-dots between the facts, drawn rather than marked up: the plate stays a handful
   of plain spans, so a feed that cannot report an age simply omits one. */
.srcstrip .sc .age::before, .srcstrip .sc .pub::before, .srcstrip .sc .n::before{
  content:"·"; margin-right:7px; color:var(--faint); font-weight:400;
}
.srcstrip .sc.ok{ --c:var(--ok); }
.srcstrip .sc.warn{ --c:var(--warn); background:color-mix(in srgb,var(--warn) 7%,var(--s2)); }
/* Dashed = the plate itself is provisional. Hollow LED = nothing is arriving now,
   we are reading what arrived last — a filled dot would claim a live source. */
.srcstrip .sc.cache{ --c:var(--warn); border-style:dashed; background:var(--s1); }
.srcstrip .sc.cache .led{ background:transparent; box-shadow:inset 0 0 0 2px var(--warn); }
.srcstrip .sc.bad{ --c:var(--bad); background:color-mix(in srgb,var(--bad) 8%,var(--s2));
  color:var(--dim); }
/* The published age is the first thing to go when space runs out — it is the
   qualifier, while our own capture age is the fact the operator acts on. */
@media (max-width:640px){ .srcstrip .sc .pub{ display:none; } }

/* live clock — the reference time for everything else on screen */
.clock{ display:inline-flex; align-items:baseline; gap:9px; user-select:none; }
.clock .time{ font-family:var(--mono); font-size:14.5px; font-weight:600; letter-spacing:.01em;
  font-variant-numeric:tabular-nums; color:var(--fg); }
.clock .sep{ width:3px; height:3px; border-radius:50%; background:var(--faint); opacity:.55; flex:0 0 auto;
  align-self:center; }
.clock .date{ font-family:var(--mono); font-size:10.5px; font-weight:500; color:var(--faint);
  letter-spacing:.06em; text-transform:uppercase; }
@media (max-width:720px){ .clock .sep,.clock .date{ display:none; } }

main{
  flex:1 1 auto; padding:26px clamp(14px,2.4vw,26px) 88px; max-width:var(--measure);
  width:100%; margin:0 auto;
}

/* ---- responsive chrome ---- */
@media (max-width:1180px){ :root{ --rail-w:var(--rail-mini); } .rail .head .wordmark,
  .rail a.it .lb, .rail .who .wtx, .rail .navgrp::after{ display:none; }
  .rail .navgrp{ justify-content:center; padding:0; font-size:0; }
  .rail .navgrp::before{ content:""; width:14px; height:1px; background:var(--stroke); }
  .rail a.it{ justify-content:center; padding:9px 0; }
  .rail .head{ padding:0; justify-content:center; }
  .rail .foot{ flex-direction:column; }
  .rail .collapse{ display:none; }
}
@media (max-width:900px){
  /* Off-canvas drawer. The rail leaves the flow entirely so the content column
     gets the whole width — on a phone in a vehicle that width is the product. */
  :root{ --rail-w:0px; }
  .rail{
    position:fixed; left:0; top:0; height:100vh; width:var(--rail-mini); flex-basis:0;
    transform:translateX(-100%); transition:transform .2s cubic-bezier(.2,.7,.3,1);
    box-shadow:var(--shadow-lg);
  }
  :root[data-rail="open"] .rail{ transform:none; width:min(272px,84vw); }
  :root[data-rail="open"] .rail .head .wordmark,
  :root[data-rail="open"] .rail a.it .lb,
  :root[data-rail="open"] .rail .who .wtx{ display:block; }
  :root[data-rail="open"] .rail .navgrp{ justify-content:flex-start; padding:0 8px; font-size:9.5px; }
  :root[data-rail="open"] .rail .navgrp::before{ display:none; }
  :root[data-rail="open"] .rail .navgrp::after{ display:block; }
  :root[data-rail="open"] .rail a.it{ justify-content:flex-start; padding:8px 9px; }
  :root[data-rail="open"] .rail .head{ padding:0 14px; justify-content:flex-start; }
  /* The drawer is full-width, so the account block goes back to one row. */
  :root[data-rail="open"] .rail .foot{ flex-direction:row; }
  .railbtn{
    display:grid; place-items:center; width:32px; height:32px; flex:0 0 auto; padding:0;
    border:1px solid var(--stroke); border-radius:var(--r2); background:var(--s2);
    color:var(--dim); cursor:pointer;
  }
  .railbtn svg{ width:17px; height:17px; }
  .scrim{ position:fixed; inset:0; z-index:65; background:rgba(0,0,0,.5);
    opacity:0; visibility:hidden; transition:.2s ease; }
  :root[data-rail="open"] .scrim{ opacity:1; visibility:visible; }
}

/* ============================= typography =============================
 * Display for titles and figures; Inter for prose; mono for anything measured.
 * The uppercase micro-caps label (.lab / h2.sec) is the system's connective
 * tissue — it is how a dense screen stays navigable without boxes everywhere.
 * ===================================================================== */
h1.page{
  font-family:var(--display); font-size:23px; font-weight:640; font-stretch:110%;
  letter-spacing:-0.004em; margin:0 0 4px; line-height:1.15;
}
.sub{ color:var(--dim); font-size:13px; margin:0 0 22px; max-width:78ch; }
h2.sec, .lab{
  font-family:var(--mono); font-size:10.5px; font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--faint);
}
h2.sec{ margin:30px 0 12px; display:flex; align-items:center; gap:10px; }
h2.sec::after{ content:""; flex:1; height:1px; background:var(--stroke); }
.mono,.num{ font-family:var(--mono); font-size:.93em; font-variant-numeric:tabular-nums; }
.dim{ color:var(--dim); } .faint{ color:var(--faint); }

/* A big figure: display face, tight, tabular. Used by KPIs and hero counters. */
.fig{ font-family:var(--display); font-weight:680; font-stretch:104%; letter-spacing:-0.02em;
  font-variant-numeric:tabular-nums; line-height:1; }

/* ============================== surfaces ==============================
 * A plate: opaque layer, hairline edge, 1 px light catch along the top. No blur —
 * see the header note. `.glass` keeps its name for the pages that still use it.
 * ===================================================================== */
.glass, .card, .veh, .kpi, .iv-card, .panel{
  background:var(--s1); border:1px solid var(--stroke); border-radius:var(--r3);
  box-shadow:var(--shadow); position:relative;
}
.glass::after, .card::after, .kpi::after, .panel::after, .iv-card::after{
  content:""; position:absolute; inset:0 0 auto; height:1px; border-radius:var(--r3) var(--r3) 0 0;
  background:linear-gradient(90deg, transparent, var(--light-catch) 22%, var(--light-catch) 78%, transparent);
  pointer-events:none;
}
.cards{ display:grid; grid-template-columns:repeat(auto-fill,minmax(288px,1fr)); gap:12px; }
.card{ padding:15px 17px; transition:border-color .16s ease, box-shadow .16s ease, transform .16s ease; }
.card.link:hover{ transform:translateY(-1px); box-shadow:var(--shadow-lg);
  border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }

/* ---- corner ticks -----------------------------------------------------
 * The signature detail: hairline registration marks at the four corners of a
 * plate, the way a technical drawing or a camera viewfinder frames its subject.
 * One pseudo-element draws a full rectangle, then a four-square mask keeps only
 * the corners.
 *
 * Inset 6 px, INSIDE the plate edge and floating in its padding. Sitting on the
 * edge instead (the obvious first try) just doubles the border and the mark
 * disappears — a registration mark has to be visibly separate from the thing it
 * registers. */
.ticks::before, .kpi::before{
  content:""; position:absolute; inset:6px; border-radius:3px; pointer-events:none;
  border:1px solid var(--tick);
  -webkit-mask:
    linear-gradient(#000,#000) 0 0/9px 9px no-repeat,
    linear-gradient(#000,#000) 100% 0/9px 9px no-repeat,
    linear-gradient(#000,#000) 0 100%/9px 9px no-repeat,
    linear-gradient(#000,#000) 100% 100%/9px 9px no-repeat;
  mask:
    linear-gradient(#000,#000) 0 0/9px 9px no-repeat,
    linear-gradient(#000,#000) 100% 0/9px 9px no-repeat,
    linear-gradient(#000,#000) 0 100%/9px 9px no-repeat,
    linear-gradient(#000,#000) 100% 100%/9px 9px no-repeat;
}

/* ---- KPI tiles: instrument plates ------------------------------------
 * Micro-caps label, one big display figure, a mono footnote. Corner ticks make a
 * row of them read as a bank of gauges rather than a row of boxes. */
.kpis{ display:grid; grid-template-columns:repeat(auto-fit,minmax(176px,1fr)); gap:12px; margin-bottom:10px; }
/* A KPI is the one surface where the ticks always earn their keep: a row of them
   then reads as a bank of gauges rather than as a row of boxes. */
.kpi{ padding:15px 16px 16px; }
.kpi .k{
  font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:.13em;
  text-transform:uppercase; color:var(--faint); display:flex; align-items:center; gap:8px;
}
.kpi .big{
  font-family:var(--display); font-size:33px; font-weight:680; font-stretch:104%;
  letter-spacing:-0.022em; margin-top:11px; font-variant-numeric:tabular-nums; line-height:1;
}
.kpi .k .ic{ width:20px; height:20px; border-radius:var(--r1); display:grid; place-items:center; font-size:11px;
  background:var(--accent-soft); color:var(--accent); flex:0 0 auto; }
.kpi .trend{ font-family:var(--mono); font-size:11px; color:var(--dim); margin-top:7px;
  font-variant-numeric:tabular-nums; }

/* ============================== chips =================================
 * Squarer than before (6 px) and mono where they carry a value: a chip is a
 * readout, not a bubble.
 * ===================================================================== */
.chips{ display:flex; gap:7px; flex-wrap:wrap; }
.chip{
  display:inline-flex; align-items:center; gap:7px; background:var(--s2); border:1px solid var(--stroke);
  padding:5px 10px; border-radius:var(--r1); font-size:12px; font-weight:520; color:var(--dim);
}
.chip b{ color:var(--fg); font-weight:640; font-family:var(--mono); font-variant-numeric:tabular-nums; }
.chip .dot{ width:7px; height:7px; border-radius:50%; flex:0 0 auto; }

/* priority chip/marker colours — a painted bar, mono label, hard edges */
.pc{ --c:var(--pna); display:inline-flex; align-items:center; gap:6px; padding:3px 8px; border-radius:var(--r1);
  font-family:var(--mono); font-size:10.5px; font-weight:600; letter-spacing:.06em; text-transform:uppercase;
  color:var(--c); background:color-mix(in srgb,var(--c) 14%,transparent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 34%,transparent); white-space:nowrap; }
.pc .dot{ width:6px; height:6px; border-radius:1px; background:var(--c);
  box-shadow:0 0 8px color-mix(in srgb,var(--c) 80%,transparent); }
.pc.p90{--c:var(--p90);} .pc.p80{--c:var(--p80);} .pc.p70{--c:var(--p70);}
.pc.p60{--c:var(--p60);} .pc.p50{--c:var(--p50);} .pc.pna{--c:var(--pna);}
.pc b{ font-weight:700; margin-right:1px; }
.pc.urgent{ animation:pulse 2.2s ease-in-out infinite; }
@keyframes pulse{ 50%{ box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--c) 34%,transparent),
  0 0 0 4px color-mix(in srgb,var(--c) 16%,transparent); } }
@media (prefers-reduced-motion: reduce){ .pc.urgent{ animation:none; } }

/* status category colours */
.cat{ --c:var(--pna); }
.cat.avail{ --c:var(--p50);} .cat.op{ --c:var(--p70);} .cat.admin{ --c:var(--pna);}

/* badges — mono micro-caps, hairline ring, no fill weight */
.badge{ font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; padding:3px 7px; border-radius:var(--r1); white-space:nowrap;
  border:1px solid transparent; }
.badge.ok{ color:var(--ok); background:color-mix(in srgb,var(--ok) 13%,transparent); border-color:color-mix(in srgb,var(--ok) 32%,transparent); }
.badge.bad{ color:var(--bad); background:color-mix(in srgb,var(--bad) 13%,transparent); border-color:color-mix(in srgb,var(--bad) 32%,transparent); }
.badge.warn{ color:var(--warn); background:color-mix(in srgb,var(--warn) 14%,transparent); border-color:color-mix(in srgb,var(--warn) 32%,transparent); }
.badge.info{ color:var(--info); background:color-mix(in srgb,var(--info) 13%,transparent); border-color:color-mix(in srgb,var(--info) 30%,transparent); }
.badge.muted{ color:var(--dim); background:var(--hi); border-color:var(--stroke); }
/* "shown from the last good copy, the source is unreachable" — the warn hue because
   the datum is still true, plus a dashed ring because the datum is no longer being
   confirmed. Same vocabulary as `.sc.cache`, so a badge on a row and the plate in
   the source strip say the same thing. Never `.badge.bad`: red in this system is OUR
   fault or an alarm, and an upstream outage is neither. */
.badge.cache{ color:var(--warn); background:color-mix(in srgb,var(--warn) 10%,transparent);
  border-color:color-mix(in srgb,var(--warn) 44%,transparent); border-style:dashed; }

/* alias rendering */
.alias{ font-weight:600; }
.alias-id{ font-family:var(--mono); color:var(--faint); font-size:.8em; margin-left:6px; }

/* ============================= controls ===============================
 * Inputs sit BELOW the plate (inset), buttons sit ABOVE it. That is the whole
 * affordance model: recessed = you type into it, raised = you press it.
 *
 * The type list below is EXHAUSTIVE on purpose. It used to name six types, so
 * `datetime-local`, `time`, `email`, `tel` and `url` fields fell straight through
 * to the browser's own widget and read as a different application on the same
 * page. A control the design system does not name is a control the browser draws.
 * ===================================================================== */
.ctl, select, textarea,
input:is([type=text],[type=search],[type=password],[type=email],[type=tel],[type=url],
         [type=number],[type=date],[type=datetime-local],[type=time],[type=month],[type=week]),
input:not([type]){
  appearance:none; -webkit-appearance:none;
  padding:8px 11px; min-height:34px; box-sizing:border-box;
  border:1px solid var(--stroke); border-radius:var(--r2); background:var(--s2); color:var(--fg);
  font-size:13px; font-family:var(--font); line-height:1.35; caret-color:var(--accent);
  outline:none; transition:.14s ease;
  box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 5%,transparent);
}
/* Hover is a hint that the field is live, not a state change — one step only. */
.ctl:hover:not(:disabled), select:hover:not(:disabled), textarea:hover:not(:disabled),
input:hover:not(:disabled):not([type=checkbox]):not([type=radio]):not([type=range]){
  border-color:color-mix(in srgb,var(--accent) 26%,var(--stroke)); }
input:focus,select:focus,textarea:focus,.ctl:focus{ border-color:color-mix(in srgb,var(--accent) 60%,var(--stroke));
  box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 4%,transparent),
             0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }
input[type=number],input[type=date],input[type=datetime-local],input[type=time],.ctl-mono{
  font-family:var(--mono); font-variant-numeric:tabular-nums; }
::placeholder{ color:var(--faint); }

/* Fallback chevron for a <select> the enhancer did not take over (data-native, or
   no JS at all). Baked per theme rather than currentColor: a background image
   cannot inherit ink. */
select{ padding-right:32px; background-image:var(--chev);
  background-repeat:no-repeat; background-position:right 10px center; background-size:15px; }
select::-ms-expand{ display:none; }
/* Windows draws the native popup itself; without these the option list is a white
   sheet on a dark page. */
option{ background:var(--s1); color:var(--fg); }

/* Native artefacts the design system replaces: spinners, and the picker glyph on
   any date field the .ndate enhancer has not claimed. */
input[type=number]{ -moz-appearance:textfield; }
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button{ -webkit-appearance:none; appearance:none; margin:0; }
::-webkit-calendar-picker-indicator{ opacity:.45; cursor:pointer; transition:.14s ease; }
::-webkit-calendar-picker-indicator:hover{ opacity:.9; }
:root[data-theme="dark"] ::-webkit-calendar-picker-indicator{ filter:invert(1); }
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]) ::-webkit-calendar-picker-indicator{ filter:invert(1); }
}

/* Chrome paints autofilled fields its own pale yellow and ignores background —
   the single loudest "generic browser form" tell on the login and user panes.
   An inset shadow the size of the box is the only thing it honours. */
input:-webkit-autofill, input:-webkit-autofill:hover,
input:-webkit-autofill:focus, input:-webkit-autofill:active{
  -webkit-text-fill-color:var(--fg); caret-color:var(--accent);
  -webkit-box-shadow:inset 0 0 0 40px var(--s2), inset 0 1px 2px color-mix(in srgb,var(--fg) 5%,transparent);
  /* Belt and braces: Chrome animates the yellow in, so outrun the animation. */
  transition:background-color 8000s ease-in-out 0s;
}
input:-webkit-autofill:focus{
  -webkit-box-shadow:inset 0 0 0 40px var(--s2), 0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }

textarea{ min-height:84px; padding:10px 12px; line-height:1.55; resize:vertical; }
/* For pasted markup / logs: mono, tighter, and horizontal scroll instead of wrap. */
textarea.ta-code{ font-family:var(--mono); font-size:12px; line-height:1.5; white-space:pre;
  overflow-wrap:normal; overflow-x:auto; }

/* File input — natively an unstyleable grey button plus a filename. The button
   part is styleable, so it gets the secondary-button look. */
input[type=file]{ padding:5px; font-size:12.5px; color:var(--dim); cursor:pointer; }
input[type=file]::file-selector-button{
  margin-right:10px; padding:5px 11px; border:1px solid var(--stroke); border-radius:var(--r1);
  background:var(--s2); color:var(--fg); font:inherit; font-weight:600; font-size:12px;
  cursor:pointer; transition:.14s ease; box-shadow:var(--shadow-sm);
}
input[type=file]::file-selector-button:hover{ background:var(--s3);
  border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }

/* States. :user-invalid, not :invalid — a required field is invalid the moment it
   renders, and colouring an untouched form red is noise, not feedback. */
.ctl:disabled, select:disabled, textarea:disabled,
input:disabled:not([type=checkbox]):not([type=radio]){
  opacity:.6; cursor:not-allowed; background:var(--hi); box-shadow:none; }
input[readonly], textarea[readonly]{ color:var(--dim); background:var(--hi); box-shadow:none; }
input:user-invalid, select:user-invalid, textarea:user-invalid{
  border-color:color-mix(in srgb,var(--bad) 55%,var(--stroke)); }
input:user-invalid:focus, select:user-invalid:focus, textarea:user-invalid:focus{
  box-shadow:0 0 0 3px color-mix(in srgb,var(--bad) 18%,transparent); }

/* Compact variant for fields that live inside a table row. */
.input-sm{ min-height:28px; padding:4px 8px; font-size:12px; border-radius:var(--r1); }
select.input-sm{ padding-right:24px; background-position:right 6px center; background-size:13px; }

label.cb{ display:inline-flex; align-items:center; gap:8px; color:var(--dim); font-size:12.5px; cursor:pointer; user-select:none; }

button.btn,.btn{
  display:inline-flex; align-items:center; gap:7px; padding:8px 15px; border:1px solid transparent;
  border-radius:var(--r2); background:var(--accent); color:var(--accent-ink); cursor:pointer;
  font-size:13px; font-weight:600; font-family:var(--font); letter-spacing:-0.002em;
  transition:.14s ease; box-shadow:var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,.16);
}
button.btn:hover,.btn:hover{ background:var(--accent-2); box-shadow:var(--shadow),
  inset 0 1px 0 rgba(255,255,255,.2); }
button.btn:active,.btn:active{ transform:translateY(.5px); }
.btn.sec{ background:var(--s2); color:var(--fg); border-color:var(--stroke); box-shadow:var(--shadow-sm); }
.btn.sec:hover{ background:var(--s3); border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); color:var(--fg); }
.btn.danger{ background:var(--bad); color:#fff; }
.btn.danger:hover{ background:color-mix(in srgb,var(--bad) 84%,#000); }
.btn.sm{ padding:5px 10px; font-size:12px; border-radius:var(--r1); }
.btn:disabled{ opacity:.45; cursor:not-allowed; box-shadow:none; }

/* pill — a secondary action that reads as a label (used for logout, filters) */
.pill{
  display:inline-flex; align-items:center; gap:7px; border:1px solid var(--stroke);
  background:var(--s2); color:var(--dim); border-radius:var(--r2); padding:6px 11px; cursor:pointer;
  font-size:12.5px; font-weight:540; font-family:var(--font); transition:.14s ease;
}
.pill:hover{ background:var(--s3); border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); color:var(--fg); }
.pill.ghost{ background:transparent; }
.pill svg{ width:15px; height:15px; }

/* square icon-only button */
.iconbtn{
  width:32px; height:32px; padding:0; display:grid; place-items:center; border:1px solid var(--stroke);
  border-radius:var(--r2); background:var(--s2); color:var(--dim); cursor:pointer; transition:.14s ease;
  flex:0 0 auto;
}
.iconbtn:hover{ color:var(--accent); background:var(--s3);
  border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }
/* A press needs to register on a touch screen in a moving vehicle, where the
   hover state never happens at all. */
.iconbtn:active{ transform:translateY(1px); background:var(--s3); }
@media (prefers-reduced-motion: reduce){ .iconbtn:active{ transform:none; } }
.iconbtn svg{ width:16px; height:16px; }

form.filters{ display:flex; gap:9px; flex-wrap:wrap; align-items:center; margin-bottom:16px; }

/* ---- segmented control — .segbar and .seg-range ----
 * A recessed track with a raised current segment: the same physical metaphor as
 * the inputs above, so "which one is on" needs no colour to be legible.
 *
 * NOT called `.seg`: that class is already taken three times over by unrelated
 * things — the status-rail phases (intervention_lib.php), the AKOS spectrum bars
 * and casovnica's histogram columns. A shared stylesheet has to check the names it
 * claims.
 *
 * `<a>` AND `<button>`: a segmented control that navigates is a link, one that
 * filters a list already on the page is a button (no href to invent, no history
 * entry, keyboard-activatable for free). Styling only the anchor form is why
 * kamere.php and degz.php each grew a private copy — `.km-tab`, `.dz-tab`. The
 * reset below (no background, no border, inherited family) is all it takes for the
 * two forms to be pixel-identical, so the choice is about semantics only. */
.segbar, .seg-range{
  display:inline-flex; padding:3px; gap:2px; border-radius:var(--r2);
  background:var(--s3); border:1px solid var(--stroke);
  box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 5%,transparent);
}
.segbar a, .seg-range a, .segbar button, .seg-range button{
  padding:5px 12px; border-radius:var(--r1); font-size:12.5px; font-weight:560; color:var(--dim);
  transition:.14s ease; white-space:nowrap; font-variant-numeric:tabular-nums;
  background:none; border:0; cursor:pointer; font-family:var(--font);
}
.segbar a:hover, .seg-range a:hover, .segbar button:hover, .seg-range button:hover{ color:var(--fg); }
.segbar a.on, .seg-range a.on, .segbar button.on, .seg-range button.on{
  color:var(--fg); background:var(--s1); box-shadow:var(--shadow-sm); font-weight:620;
}
.seg-range a.on, .seg-range button.on{ color:var(--accent); }

/* ---- custom select / listbox (track G) — replaces the native dropdown ----
 * .nsel-native is the real <select>, kept in the DOM (source of truth) but hidden.
 * The button mirrors the native input styling so it is a visual drop-in. */
.nsel-native{ position:absolute !important; width:1px; height:1px; min-height:0; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; opacity:0; pointer-events:none; }
.nsel{ position:relative; display:inline-flex; vertical-align:middle; max-width:100%; }
.nsel-btn{
  display:inline-flex; align-items:center; gap:8px; justify-content:space-between;
  width:100%; min-width:9ch; min-height:34px; text-align:left; cursor:pointer;
  padding:8px 11px; border:1px solid var(--stroke); border-radius:var(--r2);
  background:var(--s2); color:var(--fg); font:inherit; font-size:13px;
  outline:none; transition:.14s ease;
  box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 5%,transparent);
}
.nsel-btn:hover{ border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }
.nsel-btn:focus-visible{ outline:none; border-color:color-mix(in srgb,var(--accent) 60%,var(--stroke));
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }
.nsel-btn:disabled{ opacity:.5; cursor:not-allowed; }
.nsel-btn.nsel-placeholder .nsel-val{ color:var(--faint); }
.nsel-val{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.nsel-caret{ width:15px; height:15px; flex:0 0 auto; color:var(--faint); transition:transform .16s ease; }
.nsel[data-open="true"] .nsel-caret{ transform:rotate(180deg); }
.nsel[data-open="true"] .nsel-btn{ border-color:color-mix(in srgb,var(--accent) 60%,var(--stroke));
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }

/* Overlays ARE allowed to be translucent: floating above the page is exactly what
   the blur is saying, and there is only ever one of them open. */
.nsel-pop{
  position:absolute; z-index:80; top:calc(100% + 5px); left:0; min-width:100%; width:max-content;
  max-width:min(340px,92vw); max-height:min(300px,50vh); overflow-y:auto; padding:5px;
  background:var(--chrome); backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  border:1px solid var(--stroke); border-radius:var(--r3); box-shadow:var(--shadow-lg);
  animation:nselpop .13s cubic-bezier(.2,.7,.3,1);
}
.nsel-up .nsel-pop{ top:auto; bottom:calc(100% + 5px); }
@keyframes nselpop{ from{ opacity:0; transform:translateY(-4px); } to{ opacity:1; transform:none; } }
.nsel-opt{
  display:flex; align-items:center; gap:8px; padding:7px 9px; border-radius:var(--r1);
  font-size:13px; color:var(--fg); cursor:pointer; white-space:nowrap;
}
.nsel-opt::before{
  content:""; width:14px; height:14px; flex:0 0 auto;
  /* checkmark drawn with a mask so it takes the element's color (the accent on a
     selected row) in both themes — a hardcoded fill would be wrong in dark mode. */
  background-color:transparent;
  -webkit-mask:no-repeat center/12px; mask:no-repeat center/12px;
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
  mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
}
.nsel-opt[aria-selected="true"]{ color:var(--accent); font-weight:600; }
.nsel-opt[aria-selected="true"]::before{ background-color:currentColor; }
.nsel-opt.act{ background:var(--accent-soft); }
.nsel-opt.dis{ color:var(--faint); cursor:not-allowed; }
@media (prefers-reduced-motion: reduce){ .nsel-pop{ animation:none; } .nsel-caret{ transition:none; } }

/* ---- custom checkbox (track G) — replaces the native box inside label.cb ----
 * Pure CSS: the native <input> keeps every behaviour (keyboard, .checked, change,
 * form value); appearance:none just lets us paint the box. */
.cb input[type=checkbox]{
  appearance:none; -webkit-appearance:none; margin:0; flex:0 0 auto;
  width:16px; height:16px; border-radius:4px; border:1px solid color-mix(in srgb,var(--dim) 45%,var(--stroke));
  background:var(--s2); cursor:pointer; position:relative; transition:.14s ease;
  box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 6%,transparent);
}
.cb input[type=checkbox]:hover{ border-color:color-mix(in srgb,var(--accent) 50%,var(--stroke)); }
.cb input[type=checkbox]:checked{ background:var(--accent); border-color:var(--accent); box-shadow:none; }
.cb input[type=checkbox]:checked::after{
  content:""; position:absolute; inset:0;
  -webkit-mask:no-repeat center/11px; mask:no-repeat center/11px; background:var(--accent-ink);
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
  mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
}
.cb input[type=checkbox]:focus-visible{ outline:none; border-color:var(--accent);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 20%,transparent); }
.cb input[type=checkbox]:disabled{ opacity:.5; cursor:not-allowed; }

/* ---- toggle switch (track G) ----
 * Scoped to label.sw so the bare span.sw colour swatches (legends, map layer menu)
 * are untouched. */
label.sw{ display:inline-flex; align-items:center; gap:9px; cursor:pointer; user-select:none;
  color:var(--dim); font-size:12.5px; white-space:nowrap; }
label.sw input{ position:absolute; opacity:0; width:0; height:0; }
label.sw .t{ width:32px; height:19px; border-radius:999px; flex:0 0 auto; position:relative;
  background:color-mix(in srgb,var(--dim) 26%,transparent); border:1px solid var(--stroke);
  transition:.16s ease; box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 8%,transparent); }
label.sw .t::after{ content:""; position:absolute; top:1px; left:1px; width:15px; height:15px; border-radius:50%;
  background:#fff; box-shadow:0 1px 2px rgba(0,0,0,.3); transition:transform .16s cubic-bezier(.2,.7,.3,1); }
label.sw input:checked + .t{ background:var(--accent); border-color:transparent; box-shadow:none; }
label.sw input:checked + .t::after{ transform:translateX(13px); }
label.sw input:focus-visible + .t{ box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 20%,transparent); }
label.sw:has(input:checked){ color:var(--fg); }

/* ---- custom range slider (track G) ----
 * Per-context colour comes from --range-accent (default the app accent); the map's
 * flight-scrub and radar sliders set it instead of the old accent-color. */
input[type=range]{
  --range-accent:var(--accent);
  appearance:none; -webkit-appearance:none; height:20px; background:transparent; cursor:pointer;
}
input[type=range]::-webkit-slider-runnable-track{ height:5px; border-radius:999px;
  background:color-mix(in srgb,var(--dim) 28%,transparent); border:1px solid var(--stroke-2); }
input[type=range]::-moz-range-track{ height:5px; border-radius:999px;
  background:color-mix(in srgb,var(--dim) 28%,transparent); border:1px solid var(--stroke-2); }
input[type=range]::-moz-range-progress{ height:5px; border-radius:999px; background:var(--range-accent); }
input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; margin-top:-6px;
  width:15px; height:15px; border-radius:50%; background:var(--range-accent);
  border:2px solid var(--s1); box-shadow:var(--shadow-sm); transition:transform .12s ease; }
input[type=range]::-moz-range-thumb{ width:15px; height:15px; border-radius:50%; background:var(--range-accent);
  border:2px solid var(--s1); box-shadow:var(--shadow-sm); }
input[type=range]:hover::-webkit-slider-thumb{ transform:scale(1.12); }
input[type=range]:focus-visible{ outline:none; }
input[type=range]:focus-visible::-webkit-slider-thumb{ box-shadow:0 0 0 4px color-mix(in srgb,var(--range-accent) 24%,transparent); }
input[type=range]:focus-visible::-moz-range-thumb{ box-shadow:0 0 0 4px color-mix(in srgb,var(--range-accent) 24%,transparent); }

/* ---- custom date / datetime picker (track G) — replaces the native picker ---- */
.ndate-native{ position:absolute !important; width:1px; height:1px; min-height:0; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; opacity:0; pointer-events:none; }
.ndate{ position:relative; display:inline-flex; vertical-align:middle; max-width:100%; }
.ndate-btn{
  display:inline-flex; align-items:center; gap:8px; width:100%; min-width:11ch; min-height:34px; cursor:pointer;
  padding:8px 11px; border:1px solid var(--stroke); border-radius:var(--r2); background:var(--s2);
  color:var(--fg); font:inherit; font-family:var(--mono); font-size:12.5px; outline:none; transition:.14s ease;
  box-shadow:inset 0 1px 2px color-mix(in srgb,var(--fg) 5%,transparent);
  font-variant-numeric:tabular-nums;
}
.ndate-btn:hover{ border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }
.ndate-btn:focus-visible{ outline:none; border-color:color-mix(in srgb,var(--accent) 60%,var(--stroke));
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }
.ndate-btn:disabled{ opacity:.5; cursor:not-allowed; }
.ndate-btn.ndate-empty .ndate-val{ color:var(--faint); }
.ndate-ic{ width:15px; height:15px; flex:0 0 auto; color:var(--faint); }
.ndate[data-open="true"] .ndate-btn{ border-color:color-mix(in srgb,var(--accent) 60%,var(--stroke));
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }

.ndate-pop{
  position:absolute; z-index:80; top:calc(100% + 6px); left:0; width:264px; padding:12px;
  background:var(--chrome); backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  -webkit-backdrop-filter:blur(var(--blur)) saturate(var(--sat));
  border:1px solid var(--stroke); border-radius:var(--r3); box-shadow:var(--shadow-lg);
  animation:nselpop .13s cubic-bezier(.2,.7,.3,1);
}
.ndate-up .ndate-pop{ top:auto; bottom:calc(100% + 6px); }
.ndate-nav{ display:flex; align-items:center; justify-content:space-between; margin-bottom:9px; }
.ndate-my{ font-family:var(--display); font-weight:620; font-stretch:106%; font-size:13px;
  text-transform:capitalize; }
.ndate-ar2{ width:27px; height:27px; border-radius:var(--r1); border:1px solid var(--stroke); background:var(--s2);
  color:var(--dim); cursor:pointer; font-size:15px; line-height:1; transition:.14s ease; }
.ndate-ar2:hover{ color:var(--accent); border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }
/* THE MONTH GRID — minmax(0,1fr), NOT 1fr, and the day cells must be able to shrink.
 *
 * This is why the picker showed two columns of enormous dates instead of a month.
 * `1fr` is shorthand for `minmax(auto,1fr)`, so every track is floored at its item's
 * min-content width — and a day cell is a <button>, which picks up 6 px of side padding
 * from the control styles above. Seven such floors add up to far more than the 238 px
 * inside the popover, so the tracks overflowed it instead of dividing it. `aspect-ratio`
 * then made it grotesque: the square cell transferred that inflated width back as its
 * height, giving 112 px cells and an 800 px-tall popover with most of the month clipped
 * out of view.
 *
 * minmax(0,1fr) lets a track go below its content, and `min-width:0` on the cell is the
 * matching half of the same fix. Both are needed — either alone leaves the floor in
 * place. The .ndate-dows header is on the same track sizing so the weekday letters stay
 * aligned with the columns they label. */
.ndate-dows{ display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:2px; margin-bottom:4px; }
.ndate-dows span{ text-align:center; font-family:var(--mono); font-size:9.5px; font-weight:600; color:var(--faint);
  text-transform:uppercase; letter-spacing:.06em; }
.ndate-grid{ display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:2px; }
.ndate-day{ aspect-ratio:1/1; border:0; background:transparent; border-radius:var(--r1); cursor:pointer;
  color:var(--fg); font-family:var(--mono); font-size:12px; font-variant-numeric:tabular-nums; transition:.1s ease;
  /* Both zeroed deliberately: the padding is inherited from the generic control styles
     and the min-width is what `minmax(0,1fr)` above needs gone. A day cell is sized by
     its track, never by its own content. */
  min-width:0; min-height:0; padding:0; }
.ndate-day.empty{ visibility:hidden; cursor:default; }
.ndate-day:hover:not(.dis):not(.empty){ background:var(--accent-soft); }
.ndate-day.today{ box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 50%,transparent); }
.ndate-day.sel{ background:var(--accent); color:var(--accent-ink); font-weight:600; }
.ndate-day.foc{ outline:2px solid color-mix(in srgb,var(--accent) 60%,transparent); outline-offset:-2px; }
.ndate-day.dis{ color:var(--faint); opacity:.4; cursor:not-allowed; }
.ndate-time{ display:flex; align-items:center; gap:6px; margin-top:10px; padding-top:10px;
  border-top:1px solid var(--stroke-2); }
.ndate-clock{ font-size:13px; }
.ndate-hh,.ndate-mm{ width:40px; text-align:center; padding:5px 4px; border:1px solid var(--stroke);
  border-radius:var(--r1); background:var(--s2); color:var(--fg); font-family:var(--mono); font-size:12.5px;
  font-variant-numeric:tabular-nums; outline:none; }
.ndate-hh:focus,.ndate-mm:focus{ border-color:color-mix(in srgb,var(--accent) 60%,var(--stroke));
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent); }
.ndate-colon{ font-family:var(--mono); font-weight:700; color:var(--dim); }
.ndate-now{ margin-left:auto; padding:5px 10px; border:1px solid var(--stroke); border-radius:var(--r1);
  background:var(--s2); color:var(--dim); cursor:pointer; font-family:var(--mono); font-size:10.5px;
  letter-spacing:.08em; text-transform:uppercase; transition:.14s ease; }
.ndate-now:hover{ color:var(--accent); border-color:color-mix(in srgb,var(--accent) 34%,var(--stroke)); }
@media (prefers-reduced-motion: reduce){ .ndate-pop{ animation:none; } }

/* ---- waveform audio player (track G) — replaces native <audio controls> ---- */
.nplay{ display:flex; align-items:center; gap:12px; width:100%; padding:7px 11px;
  border:1px solid var(--stroke); border-radius:var(--r3); background:var(--s2); }
.nplay audio{ display:none; }
.nplay-btn{ flex:0 0 auto; width:36px; height:36px; border-radius:50%; border:0; cursor:pointer;
  display:grid; place-items:center; background:var(--accent); color:var(--accent-ink);
  transition:.14s ease; box-shadow:var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,.18); }
.nplay-btn:hover{ background:var(--accent-2); }
.nplay-btn:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 28%,transparent); }
.nplay-btn svg{ width:17px; height:17px; fill:currentColor; }
.nplay-mid{ display:flex; align-items:center; gap:10px; flex:1; min-width:0; }
.nplay-wave{ flex:1; height:38px; min-width:80px; cursor:pointer; display:block; touch-action:none;
  /* colours read by the canvas painter; theme-aware */
  --wave-played:var(--accent); --wave-un:color-mix(in srgb,var(--dim) 42%,transparent); }
.nplay-t{ font-family:var(--mono); font-size:11px; color:var(--dim); font-variant-numeric:tabular-nums; flex:0 0 auto; }

/* ============================== tables ================================
 * Dense, ruled, mono where it counts. The header is a recessed strip with
 * micro-caps labels; rows are separated by hairlines, not by zebra fill, so a
 * coloured status cell is the only saturated thing in the grid.
 * ===================================================================== */
.panel{ overflow:hidden; }
.wrap{ overflow-x:auto; border-radius:var(--r3); }
table{ border-collapse:separate; border-spacing:0; width:100%; }
thead th{
  position:sticky; top:0; z-index:2; background:var(--s2);
  color:var(--faint); font-family:var(--mono); font-size:10px; font-weight:600; text-transform:uppercase;
  letter-spacing:.11em; text-align:left; padding:9px 13px; border-bottom:1px solid var(--stroke);
  white-space:nowrap;
}
tbody td{ padding:9px 13px; border-bottom:1px solid var(--stroke-2); vertical-align:top; }
tbody tr{ transition:background .1s ease; }
tbody tr:hover{ background:var(--hi); }
tbody tr:last-child td{ border-bottom:none; }
tbody td .mono, tbody td.num{ font-family:var(--mono); font-variant-numeric:tabular-nums; }
/* `pre-wrap` is right for a cell whose content IS raw dispatch text — the line
 * breaks the dispatcher typed are information. It is wrong for a cell that wraps
 * that text in elements, because then the template's own indentation between the
 * tags renders as blank lines: komunikacije.php's message column was drawing rows
 * ~50 px taller than their content, with the text pushed down a line. Structured
 * content opts back out; a bare-text cell is untouched. */
.txt{ max-width:560px; white-space:pre-wrap; word-break:break-word; }
.txt:has(> div), .txt:has(> p), .txt:has(> table){ white-space:normal; }

/* empty state — a plate with a drawn mark, not an emoji shrug */
.empty{ padding:56px 24px; color:var(--dim); text-align:center; }
.empty .big{ font-family:var(--display); font-size:30px; opacity:.35; margin-bottom:10px; font-stretch:110%; }

/* pager */
.pager{ display:flex; gap:7px; align-items:center; margin-top:18px; justify-content:center; }
.pager a,.pager span{ padding:7px 12px; border:1px solid var(--stroke); border-radius:var(--r2);
  background:var(--s2); font-size:12.5px; font-weight:540; transition:.14s ease; }
.pager a:hover{ border-color:color-mix(in srgb,var(--accent) 40%,var(--stroke)); color:var(--accent); }
.pager .cur{ color:var(--faint); background:transparent; border-color:transparent;
  font-family:var(--mono); font-size:11.5px; letter-spacing:.06em; }

audio{ height:34px; vertical-align:middle; max-width:100%; }
.fade-in{ animation:fade .34s cubic-bezier(.2,.7,.3,1) both; }
@keyframes fade{ from{ opacity:0; transform:translateY(5px);} }
@media (prefers-reduced-motion: reduce){ .fade-in{ animation:none; } }

/* ====================== glossary abbreviations (track D) ==================
 * Recognised dispatch shorthand becomes a focusable button with a popover.
 *
 * A BUTTON, not a span with :hover — keyboard and touch users must be able to
 * reach it. The older data-detail tooltips elsewhere in the app are hover-only
 * and therefore unreachable, which is the mistake this avoids.
 * ====================================================================== */
.gl{
  font:inherit; color:inherit; background:none; border:0; padding:0; cursor:help;
  border-bottom:1px dotted color-mix(in srgb,var(--accent) 60%,transparent);
  position:relative;
}
.gl:hover, .gl:focus-visible{ color:var(--accent); border-bottom-style:solid; }
.gl:focus-visible{ outline:2px solid color-mix(in srgb,var(--accent) 60%,transparent);
  outline-offset:2px; border-radius:2px; }
/* Unverified seeds are marked so a guess is never presented as fact. */
.gl-unverified{ border-bottom-color:color-mix(in srgb,var(--warn) 65%,transparent); }
.gl-unverified::after{ content:"?"; font-size:.7em; vertical-align:super; color:var(--warn); }

/* The popover, shown on hover AND focus AND tap. */
.gl::before{
  content:attr(data-gl);
  position:absolute; left:50%; bottom:calc(100% + 7px); transform:translateX(-50%);
  min-width:max-content; max-width:280px; white-space:normal; text-align:left;
  padding:7px 10px; border-radius:var(--r2); font-family:var(--font); font-size:12px; font-weight:500;
  line-height:1.4; letter-spacing:0;
  color:var(--fg); background:var(--chrome); border:1px solid var(--stroke);
  box-shadow:var(--shadow-lg); backdrop-filter:blur(var(--blur));
  opacity:0; visibility:hidden; transition:opacity .12s ease; pointer-events:none; z-index:70;
}
.gl:hover::before, .gl:focus-visible::before, .gl.gl-open::before{ opacity:1; visibility:visible; }

/* ================== stale-data banner (plan item C10) ====================
 * A control room must never quietly show old data. If ingest stops, the dashboard
 * keeps displaying the last dispatch and looks perfectly healthy — so the absence
 * of new data has to be stated, not inferred. This is a safety affordance, not
 * decoration, which is why it is loud and sticky rather than a toast.
 *
 * Hazard stripes: at a glance, from across the room, diagonal hatching reads as
 * "instrument fault" faster than any wording does.
 * ===================================================================== */
#stale-banner{
  display:none; position:sticky; top:calc(var(--top-h) + 8px); z-index:58;
  margin:0 0 16px; padding:11px 15px; border-radius:var(--r3);
  font-size:13px; font-weight:540; line-height:1.45;
  border:1px solid transparent; box-shadow:var(--shadow);
}
#stale-banner.on{ display:flex; align-items:center; gap:11px; }
#stale-banner .ic{ font-size:16px; flex:0 0 auto; }
#stale-banner .age{ font-family:var(--mono); font-variant-numeric:tabular-nums; font-weight:600; }
#stale-banner.warn{
  color:var(--warn); border-color:color-mix(in srgb,var(--warn) 38%,transparent);
  background:
    repeating-linear-gradient(135deg, color-mix(in srgb,var(--warn) 10%,transparent) 0 9px, transparent 9px 18px),
    color-mix(in srgb,var(--warn) 8%,var(--s1));
}
#stale-banner.bad{
  color:var(--bad); border-color:color-mix(in srgb,var(--bad) 42%,transparent);
  background:
    repeating-linear-gradient(135deg, color-mix(in srgb,var(--bad) 13%,transparent) 0 9px, transparent 9px 18px),
    color-mix(in srgb,var(--bad) 9%,var(--s1));
  animation:stalepulse 3s ease-in-out infinite;
}
@keyframes stalepulse{ 50%{ box-shadow:0 0 0 4px color-mix(in srgb,var(--bad) 16%,transparent); } }
@media (prefers-reduced-motion: reduce){ #stale-banner.bad{ animation:none; } }

/* ============ time-to-target ring (C2) + unit status rail (C3) ============
 * Both read from data the platform already parses: priority_catalog() carries the
 * DSZ doctrine response target, status_catalog()/status_phase() carry the phases.
 * Nothing new is stored to draw either of them.
 * ===================================================================== */

/* ---- the ring ---------------------------------------------------------- */
.tring{ flex:0 0 auto; overflow:visible; }
.tring .tr-bg{ stroke:color-mix(in srgb,var(--dim) 22%,transparent); }
.tring .tr-fg{ stroke:var(--accent); transition:stroke-dasharray .6s linear, stroke .3s ease; }
.tring .tr-t{
  font-family:var(--mono); font-size:10px; font-weight:600; fill:var(--dim);
  font-variant-numeric:tabular-nums;
}
/* Approaching the doctrine deadline, then past it. The colour IS the message, so
 * it also carries a text label via aria-label — never colour alone. */
.tring.warn .tr-fg{ stroke:var(--warn); }
.tring.warn .tr-t{ fill:var(--warn); }
.tring.over .tr-fg{ stroke:var(--bad); }
.tring.over .tr-t{ fill:var(--bad); font-weight:700; }
.tring.over{ animation:tringpulse 2.5s ease-in-out infinite; }
@keyframes tringpulse{ 50%{ opacity:.5; } }
@media (prefers-reduced-motion: reduce){ .tring.over{ animation:none; } }

/* ---- the unit status rail --------------------------------------------- */
/* One horizontal track per unit: Sprejeto → Na poti → Na kraju → Pri pacientu →
 * Odhod → Na cilju → Prost, with the time spent in each segment. This is the
 * visual dispatch products are recognised by, and it replaces reading a list of
 * timestamps and doing the subtraction in your head.
 *
 * Now built as a FLIGHT STRIP: segments butt against each other with a single
 * hairline between them and a 2 px painted cap on top, so the sequence reads as
 * one continuous track rather than seven separate pills. */
.srail{ display:flex; align-items:stretch; gap:0; margin:11px 0 4px;
  border:1px solid var(--stroke); border-radius:var(--r2); overflow:hidden; background:var(--s2); }
.srail .seg{
  flex:1 1 0; min-width:0; position:relative;
  padding:8px 9px 7px; border-left:1px solid var(--stroke);
  background:color-mix(in srgb,var(--c,var(--pna)) 11%,transparent);
}
.srail .seg:first-child{ border-left:0; }
.srail .seg::before{
  content:""; position:absolute; inset:0 0 auto; height:2px; background:var(--c,var(--pna));
}
/* A phase that has not happened yet is present but plainly empty, so the shape of
 * the whole call is visible from the first status onward. */
.srail .seg.pending{ background:transparent; opacity:.5; }
.srail .seg.pending::before{ background:repeating-linear-gradient(90deg,
  var(--stroke) 0 3px, transparent 3px 6px); }
.srail .seg.now{ background:color-mix(in srgb,var(--c) 18%,transparent); }
.srail .seg.now::before{ box-shadow:0 0 10px color-mix(in srgb,var(--c) 90%,transparent); }
.srail .seg .ph{
  display:block; font-family:var(--mono); font-size:9.5px; font-weight:600; letter-spacing:.09em;
  text-transform:uppercase; color:color-mix(in srgb,var(--c,var(--dim)) 72%,var(--fg));
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.srail .seg.pending .ph{ color:var(--faint); }
.srail .seg .tm{
  display:block; margin-top:4px; font-family:var(--mono); font-size:11.5px;
  font-weight:600; color:var(--fg); font-variant-numeric:tabular-nums;
}
.srail .seg .dur{ font-size:10.5px; color:var(--dim); font-weight:400; }
.srail .seg.pending .tm{ color:var(--faint); font-weight:400; }
/* Phase colours reuse the priority tokens so the whole app shares one palette. */
.srail .seg.enroute{ --c:var(--p70); }
.srail .seg.onscene{ --c:var(--p60); }
.srail .seg.dest{ --c:var(--p80); }
.srail .seg.returning,.srail .seg.free{ --c:var(--p50); }
.srail .seg.other{ --c:var(--pna); }

@media (max-width:720px){
  /* Stacked on a phone: seven columns of 40px is unreadable, and the order still
     reads top-to-bottom as the sequence it is. */
  .srail{ flex-direction:column; }
  .srail .seg{ display:flex; align-items:baseline; gap:9px; padding:6px 10px;
    border-left:0; border-top:1px solid var(--stroke); }
  .srail .seg:first-child{ border-top:0; }
  .srail .seg::before{ inset:0 auto 0 0; width:2px; height:auto; }
  .srail .seg .ph{ flex:0 0 92px; }
  .srail .seg .tm{ margin-top:0; }
}

/* Dense variant, for the rail inside a table cell in an expanded dashboard row.
 * The durations go: inside a ~190px cell the phase name and the clock time are what
 * you read, and "+8:40" repeated in six segments is what makes it illegible. */
.srail.sm, .utab .tlcell .srail{ margin:2px 0; }
.srail.sm .seg, .utab .tlcell .srail .seg{ padding:5px 7px 4px; }
.srail.sm .seg .ph, .utab .tlcell .srail .seg .ph{ font-size:8.5px; letter-spacing:.06em; }
.srail.sm .seg .tm, .utab .tlcell .srail .seg .tm{ margin-top:2px; font-size:10.5px; }
.srail.sm .seg .dur, .utab .tlcell .srail .seg .dur{ display:none; }

/* ---- the CALL rail ------------------------------------------------------
 * Two rail idioms, deliberately: .srail above is ONE UNIT's flight strip, six
 * phases wide and ~45px tall, and it is right for a page or a table cell that has
 * room for it. This is the CALL's own four-beat progression — Poziv → Na poti →
 * Na kraju → Zaključeno — which has to fit inside a ~130px feed row alongside three
 * other lines. Hence dots and connectors rather than boxed segments: at this height
 * a segment cannot hold both a label and a time.
 *
 * They are visually distinct on purpose. An expanded dashboard row shows both at
 * once, and if they looked alike the aggregate would read as a duplicate of the
 * per-unit one. See call_rail_html() / status_rail_html() in intervention_lib.php.
 * -------------------------------------------------------------------- */
.crail{ list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); }
.cr-n{ --c:var(--pna); position:relative; min-width:0; padding-top:3px;
  display:grid; grid-template-rows:auto auto auto; row-gap:2px; }
/* The connector runs from the previous node to this one, and is painted only when
   this node has actually been reached — so the filled length IS the progress. */
.cr-n + .cr-n::before{ content:""; position:absolute; left:0; right:50%; top:7px;
  height:2px; background:var(--stroke-2); }
.cr-n.done::before, .cr-n.now::before{ background:var(--c); }
.cr-n .cr-d{ width:9px; height:9px; border-radius:50%; box-sizing:border-box; position:relative;
  z-index:1; border:2px solid var(--stroke-2); background:var(--s1); }
.cr-n.done .cr-d, .cr-n.now .cr-d{ border-color:var(--c); background:var(--c); }
.cr-n.now .cr-d{ box-shadow:0 0 0 3px color-mix(in srgb,var(--c) 22%,transparent); }
.cr-n .cr-l{ font-family:var(--mono); font-size:9.5px; font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; color:var(--dim); padding-right:8px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.cr-n.pending .cr-l{ color:var(--faint); }
.cr-n.now .cr-l{ color:color-mix(in srgb,var(--c) 70%,var(--fg)); }
.cr-n .cr-t{ font-size:11px; font-weight:600; color:var(--fg); font-variant-numeric:tabular-nums; }
.cr-n.pending .cr-t{ color:var(--faint); font-weight:400; }
/* Phase colours reuse the priority tokens, exactly as .srail does. --accent for
   Poziv rather than a priority token, because "the call exists" is not a phase of
   response — using --p70 there would imply somebody was already moving. */
.cr-n.poziv{ --c:var(--accent); }
.cr-n.enroute{ --c:var(--p70); }
.cr-n.onscene{ --c:var(--p60); }
.cr-n.end.done{ --c:var(--p50); }
/* Past the doctrine target with nobody rolling — the one thing the rail alarms about. */
.cr-n.overdue{ --c:var(--bad); }
.cr-n.overdue .cr-l, .cr-n.overdue .cr-t{ color:var(--bad); }
/* A card whose units cannot report status at all (a pager activation): one line
   saying so, not four nodes of dashes claiming nothing has happened. */
.crail.cr-none{ display:flex; align-items:baseline; gap:9px; }
.crail.cr-none .cr-l{ color:var(--dim); }
.crail.cr-none .cr-no{ font-size:11px; color:var(--faint); }

/* ---- source badges (shared: dashboard rows and their expanded records) --- */
.sb{ font-size:10.5px; font-weight:680; letter-spacing:.02em; padding:3px 8px; border-radius:7px;
  border:1px solid transparent; white-space:nowrap; text-transform:uppercase; }
.sb.dsz{ color:var(--accent-ink); background:var(--accent); }
.sb.spin{ color:var(--info); background:color-mix(in srgb,var(--info) 15%,transparent); border-color:color-mix(in srgb,var(--info) 32%,transparent); }
.sb.reco{ color:var(--info); background:color-mix(in srgb,var(--info) 9%,transparent); border-color:color-mix(in srgb,var(--info) 22%,transparent); }
.sb.ignis{ color:var(--warn); background:color-mix(in srgb,var(--warn) 14%,transparent); border-color:color-mix(in srgb,var(--warn) 30%,transparent); }
.sb.fire{ color:var(--bad); background:color-mix(in srgb,var(--bad) 13%,transparent); border-color:color-mix(in srgb,var(--bad) 28%,transparent); }
.sb.pocsag{ color:var(--dim); background:color-mix(in srgb,var(--dim) 12%,transparent); border-color:var(--stroke); }
.sb.muted{ color:var(--dim); background:color-mix(in srgb,var(--dim) 10%,transparent); border-color:var(--stroke); }

/* ---- the UNIT CHIP (shared: dashboard tape, wall board, detail hero) -----
 * One unit committed to a call: its name, the phase it is in (as a painted left
 * edge, from the same priority tokens the rails use), the status text and how long
 * it has been in it. It lived in index.php until the wall board started drawing the
 * same chips for the same units — at which point a private copy on each page was
 * two vocabularies for one thing. Page-specific sizing (the tape's max-width, what
 * a narrow screen sheds) stays with the page.
 * ------------------------------------------------------------------------ */
.uch{ --c:var(--pna); display:inline-flex; align-items:center; gap:6px; flex:0 0 auto;
  max-width:212px; padding:3px 9px 3px 7px; border-radius:var(--r1);
  border:1px solid var(--stroke); border-left:2px solid var(--c);
  background:color-mix(in srgb,var(--c) 7%,var(--s2)); white-space:nowrap; min-width:0; }
.uch .ic{ width:14px; flex:0 0 auto; text-align:center; font-size:11.5px; line-height:1; }
.uch .n{ font-family:var(--mono); font-size:11.5px; font-weight:640; color:var(--fg);
  overflow:hidden; text-overflow:ellipsis; }
.uch .s{ font-size:11px; font-weight:560; color:color-mix(in srgb,var(--c) 62%,var(--fg));
  overflow:hidden; text-overflow:ellipsis; }
.uch .e{ font-size:10.5px; color:var(--dim); font-variant-numeric:tabular-nums;
  padding-left:7px; border-left:1px solid var(--stroke-2); }
/* "×2" — the vehicle answered on two radios. Deliberately quiet: it qualifies the
   unit, it is not a second unit. */
.uch .nr{ font-size:10px; font-weight:600; color:var(--faint); flex:0 0 auto; }
.uch.enroute{ --c:var(--p70); } .uch.onscene{ --c:var(--p60); }
.uch.dest{ --c:var(--p80); } .uch.returning,.uch.free{ --c:var(--p50); }
.uch.act{ --c:var(--uc,var(--pna)); }
.uch.act .s{ color:var(--faint); font-style:italic; }
/* "+9" — the units there was no width for. Quiet, because it is a count rather than
   a unit in its own right, but PRESENT: a strip that silently stops at six reads as
   a six-unit call. No cursor:pointer — it promises no interaction it does not have. */
.uch.more{ --c:var(--stroke); color:var(--dim); font-size:11px; font-weight:640;
  font-family:var(--mono); font-style:normal; }
.uch.nodata{ border-left-color:var(--stroke); color:var(--faint); font-size:11px;
  background:var(--s2); padding:3px 9px; }

/* ---- one source record, and why records were merged --------------------- */
.op-rec{ display:flex; gap:11px; align-items:flex-start; padding:8px 10px; border-radius:10px;
  border:1px solid var(--stroke-2); background:var(--hi); margin-bottom:6px; }
.op-rec .rt{ font-family:var(--mono); font-size:12px; color:var(--dim); min-width:40px; }
.op-rec .rmain{ flex:1; min-width:0; }
.op-rec .rmain .ra{ font-weight:560; font-size:13px; }
.op-rec .rmain .rx{ color:var(--dim); font-size:12px; margin-top:2px; word-break:break-word; }
.op-units{ display:flex; flex-wrap:wrap; gap:5px; margin-top:6px; }
.op-u{ font-size:11px; padding:2px 8px; border-radius:20px; background:var(--glass-2);
  border:1px solid var(--stroke); color:var(--fg); }
.op-u small{ color:var(--faint); }
.op-gps{ margin-left:6px; }
.op-conf{ display:inline-flex; align-items:center; gap:6px; font-size:11px; color:var(--dim); }
.op-conf .n{ font-family:var(--mono); font-weight:640; }
/* The audit trail for an automatic merge. Tinted, because it is the machine
   explaining itself rather than dispatch data. */
.op-links{ margin-top:6px; padding:9px 11px; border-radius:10px;
  background:color-mix(in srgb,var(--accent) 5%,transparent);
  border:1px solid color-mix(in srgb,var(--accent) 16%,transparent);
  font-size:11.5px; color:var(--dim); }
.op-links .lk{ display:block; margin:2px 0; }
.op-links code{ font-family:var(--mono); color:var(--fg); font-size:10.5px; }

/* ---- one POCSAG page and its recipients --------------------------------
   Same frame as .op-rec (it sits in the same section rhythm), but the RIC chips
   are monospaced: an operator reading this is comparing capcode NUMBERS, and
   proportional digits make that harder than it needs to be. */
.op-pg{ display:flex; gap:11px; align-items:flex-start; padding:8px 10px; border-radius:10px;
  border:1px solid var(--stroke-2); background:var(--hi); margin-bottom:6px; }
.op-pg .rt{ font-family:var(--mono); font-size:12px; color:var(--dim); min-width:40px; }
.op-pg .rmain{ flex:1; min-width:0; }
.op-pg .pgm{ font-size:11.5px; color:var(--dim); }
.op-pg .pgr{ display:flex; flex-wrap:wrap; gap:5px; margin-top:6px; }
.op-pg .ric{ display:inline-flex; align-items:center; gap:5px; font-size:11px;
  padding:2px 8px; border-radius:20px; background:var(--glass-2);
  border:1px solid var(--stroke); color:var(--fg); }
.op-pg .ric code{ font-family:var(--mono); font-size:10.5px; color:var(--dim); }
.op-pg .ric .dot{ width:7px; height:7px; border-radius:50%; flex:none; }
/* A RIC nobody has bound to a unit. Warned rather than hidden — it is the one row
   here that represents work outstanding, and dropping it would make the page look
   fully understood when it isn't. */
.op-pg .ric.unres{ border-color:color-mix(in srgb,var(--warn) 40%,transparent);
  background:color-mix(in srgb,var(--warn) 8%,transparent); }
.op-pg .ric.unres code{ color:var(--warn); }
/* Muted, not removed: the page went out regardless — `ignore` is a display
   preference upstream, not evidence that nothing happened. */
.op-pg .ric.ign{ opacity:.5; }

/* ---- live / done lamps (shared: detail hero and dashboard rows) --------- */
.live-dot, .lamp.live{ display:inline-flex; align-items:center; gap:7px; font-size:10.5px;
  font-weight:740; letter-spacing:.09em; text-transform:uppercase; color:var(--ok); }
.live-dot i, .lamp.live i{ width:8px; height:8px; border-radius:50%; background:var(--ok);
  box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 70%,transparent); }
@keyframes lpulse{ 0%{ box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 55%,transparent);}
  70%{ box-shadow:0 0 0 7px transparent;} 100%{ box-shadow:0 0 0 0 transparent;} }
.live-dot i{ animation:lpulse 1.8s ease-out infinite; }
/* Only the two priorities that should catch the eye unbidden pulse in the FEED:
   24 infinite animations on a page that also runs the radar and the plotted field
   is a compositor cost with no extra information in it. */
.ivc.p90 .lamp.live i, .ivc.p80 .lamp.live i{ animation:lpulse 1.8s ease-out infinite; }
.done-dot, .lamp.done{ display:inline-flex; align-items:center; gap:7px; font-size:10.5px;
  font-weight:740; letter-spacing:.09em; text-transform:uppercase; color:var(--faint); }
.done-dot i, .lamp.done i{ width:8px; height:8px; border-radius:50%; background:var(--faint); }
/* The third state: no source reports a closure. Amber rather than grey, because
   "we don't know" is a gap in the picture and should be visible as one — but
   DELIBERATELY UNANIMATED. A pulse is what the eye reads as "live", and this lamp
   is precisely the case where we cannot say that. */
.lamp.unk{ display:inline-flex; align-items:center; gap:7px; font-size:10.5px;
  font-weight:740; letter-spacing:.09em; text-transform:uppercase; color:var(--warn); }
.lamp.unk i{ width:8px; height:8px; border-radius:50%; background:var(--warn); opacity:.75; }
@media (prefers-reduced-motion: reduce){ .live-dot i, .lamp.live i{ animation:none; } }

/* ---- unit primitives (shared) -------------------------------------------
 * These were page-local to intervencija.php until the dashboard began rendering
 * its own unit table from the same data. Both surfaces must describe a unit
 * identically — a status chip that means one thing on the feed and another on the
 * detail page is worse than having only one of them. */
.state{ display:inline-flex; align-items:center; gap:6px; padding:3px 9px; border-radius:8px;
  font-size:11.5px; font-weight:640; color:var(--c); background:color-mix(in srgb,var(--c) 14%,transparent);
  border:1px solid color-mix(in srgb,var(--c) 30%,transparent); }
.state .dot{ width:6px; height:6px; border-radius:50%; background:var(--c); }
.since{ font-size:11px; color:var(--faint); font-variant-numeric:tabular-nums; }
.vtype{ display:inline-block; padding:1px 7px; border-radius:6px; font-size:10.5px; font-weight:700;
  letter-spacing:.03em; color:var(--accent); background:color-mix(in srgb,var(--accent) 12%,transparent);
  border:1px solid color-mix(in srgb,var(--accent) 26%,transparent); }
.crewpill{ display:inline-flex; align-items:center; gap:7px; padding:3px 9px; border-radius:8px;
  border:1px solid var(--stroke); background:var(--glass-2); font-size:11.5px; box-shadow:var(--shadow-sm); }
.crewpill .role{ color:var(--dim); font-weight:520; }
.crewpill b{ font-weight:640; }
.crew-none{ color:var(--faint); font-size:11.5px; font-style:italic; }
/* Not the same statement as "nobody is aboard": a stale login is a data-freshness
   problem and has to look like a warning, not like an empty vehicle. */
.crew-none.stale, .stale{ color:var(--warn); font-style:normal; }
.slachip{ display:inline-flex; align-items:baseline; gap:6px; padding:3px 9px; border-radius:8px;
  border:1px solid var(--stroke-2); background:var(--hi); font-size:11px; color:var(--dim); }
.slachip b{ font-family:var(--mono); font-variant-numeric:tabular-nums; color:var(--fg);
  font-weight:640; font-size:12px; }
.raw{ font-family:var(--mono); font-size:11.5px; color:var(--dim); white-space:pre-wrap;
  word-break:break-word; background:var(--hi); border:1px solid var(--stroke-2);
  border-radius:9px; padding:9px 11px; margin-top:2px; }

/* ---- the intervention map pin (shared) ----------------------------------
 * Was page CSS in zemljevid.php until the dashboard grew a map of the same
 * interventions beside its tape. One component, three feed shapes, so a pin means
 * the same thing on both pages — the markup builder is assets/mappins.js.
 *
 * The fill is --pgc, set inline per pin: these sit on a photographic basemap, so
 * the colour is a hard-coded hue from mappins.js's palette rather than a theme
 * token that would vanish against dark tiles. */
.mlmarker{ line-height:0; cursor:pointer; }
.gpin{ position:relative; line-height:0; }
.gpin .pin{ width:30px; height:30px; display:grid; place-items:center; position:relative; z-index:2;
  border:2px solid #fff; background:var(--pgc);
  background-image:linear-gradient(135deg,rgba(255,255,255,.28),rgba(0,0,0,.14));
  box-shadow:0 4px 6px rgba(0,0,0,.34), 0 1px 1px rgba(0,0,0,.24);
  transition:transform .16s cubic-bezier(.34,1.56,.64,1); }
.gpin.dsz  .pin{ border-radius:50% 50% 50% 0; transform:rotate(-45deg); }   /* classic teardrop */
.gpin.pz   .pin{ border-radius:50% 50% 50% 0; transform:rotate(-45deg); }   /* fire watch — teardrop */
.gpin.spin .pin{ border-radius:11px 11px 11px 0; transform:rotate(-45deg); }/* rounded square  */
.gpin.pocsag .pin{ border-radius:50%; }                                     /* circle          */
.gpin .pin svg{ width:15px; height:15px; color:#fff; }
.gpin .pin .num{ color:#fff; font-family:Inter,system-ui,sans-serif; font-weight:800; font-size:12px;
  line-height:1; letter-spacing:-.5px; }
.gpin.dsz .pin>*, .gpin.spin .pin>*, .gpin.pz .pin>*{ transform:rotate(45deg); }  /* glyph upright */
.gpin.pocsag:hover .pin{ transform:translateY(-3px) scale(1.09); }
.gpin.dsz:hover .pin, .gpin.spin:hover .pin, .gpin.pz:hover .pin{ transform:translateY(-3px) rotate(-45deg) scale(1.09); }
.gpin:hover{ z-index:1000; }
/* soft halo behind the pin; animates only for urgent scenes */
.gpin .halo{ position:absolute; left:50%; top:15px; width:34px; height:34px; margin:-17px 0 0 -17px;
  border-radius:50%; z-index:1; pointer-events:none;
  background:radial-gradient(circle, color-mix(in srgb,var(--pgc) 55%,transparent) 0%, transparent 68%); opacity:0; }
.gpin.urgent .halo{ animation:pin-pulse 1.9s cubic-bezier(.4,0,.2,1) infinite; }
@keyframes pin-pulse{ 0%{ transform:scale(.55); opacity:.75; } 70%{ opacity:0; } 100%{ transform:scale(2.6); opacity:0; } }
@media (prefers-reduced-motion: reduce){ .gpin.urgent .halo{ animation:none; } }
/* The dashboard's own state: the pin whose tape row is open. A white ring rather
   than a size change, so the marker does not move under the pointer that selected it. */
.gpin.sel .pin{ box-shadow:0 0 0 3px #fff, 0 0 0 5px color-mix(in srgb,var(--pgc) 70%,transparent),
  0 4px 8px rgba(0,0,0,.4); }
/* Not on this page of the tape — still real, still plotted, but dimmed so the pins
   you can actually open read as the foreground. */
.gpin.off .pin{ opacity:.5; }

/* ---- screen-reader-only text ------------------------------------------- */
/* Not display:none — that removes it from the accessibility tree too, which is the
   opposite of the point. Used wherever state is carried by colour or geometry (the
   rails, the lamps) so it is never carried by colour ALONE. */
.sr{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0; }

/* Off-sequence events (Kosilo, Kava, Predaja B/P) are notches, not phases: they
 * happen during a call without being part of its progression. */
.snotch{ display:inline-flex; align-items:center; gap:5px; margin:2px 6px 2px 0;
  padding:2px 7px; border-radius:var(--r1); font-size:11px; font-weight:520;
  color:var(--dim); background:var(--hi); border:1px solid var(--stroke-2); }
.snotch .mono{ font-size:10.5px; color:var(--faint); }

/* The exact status list, kept under the rail. Collapsed by default: the rail
 * answers "how is this going", the list answers "what happened, to the second" —
 * an after-action question that does not need to be on screen during the call. */
.tl-details{ margin-top:7px; }
.tl-details > summary{
  cursor:pointer; font-family:var(--mono); font-size:10px; font-weight:600; color:var(--faint);
  letter-spacing:.12em; text-transform:uppercase; list-style:none;
  padding:3px 0; user-select:none;
}
.tl-details > summary::-webkit-details-marker{ display:none; }
.tl-details > summary::before{ content:"▸ "; color:var(--faint); }
.tl-details[open] > summary::before{ content:"▾ "; }
.tl-details > summary:hover{ color:var(--accent); }
.snotches{ margin-top:2px; }

/* Hero ring block (C2) — the larger ring beside the elapsed timer. */
.hero-ring{ display:flex; align-items:center; gap:9px; margin-top:9px; justify-content:flex-end; }
.hero-ring .hero-ring-l{ font-family:var(--mono); font-size:10px; font-weight:600; color:var(--faint);
  letter-spacing:.1em; text-transform:uppercase; white-space:nowrap; }
.hero-ring .tring .tr-t{ font-size:13px; }

/* ======================== analytics (analitika.php) =====================
 * Every chart here is rendered server-side as CSS boxes or inline SVG — there is
 * no client charting library, so nothing has to be exempted from the strict CSP.
 * Categorical hues come from --cat-1..5 (a distinct order from priority --p*). */

.an-head{ display:flex; align-items:flex-end; justify-content:space-between; gap:16px;
  flex-wrap:wrap; margin-bottom:16px; }
.an-head h1{ margin:0; font-family:var(--display); font-size:23px; font-weight:640; font-stretch:110%;
  letter-spacing:-0.004em; }
.an-head .sub{ color:var(--dim); font-size:13px; margin-top:3px; }

/* section title above each panel */
.an-sect{ margin:26px 0 11px; display:flex; align-items:center; gap:10px; }
.an-sect h2{ margin:0; font-family:var(--mono); font-size:10.5px; font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--faint); }
.an-sect .note{ font-size:12px; color:var(--faint); }
.an-sect::after{ content:""; flex:1; height:1px; background:var(--stroke); }

.an-grid{ display:grid; gap:12px; grid-template-columns:repeat(12,1fr); }
.an-grid > .col-4{ grid-column:span 4; } .an-grid > .col-6{ grid-column:span 6; }
.an-grid > .col-8{ grid-column:span 8; } .an-grid > .col-12{ grid-column:span 12; }
@media (max-width:920px){ .an-grid > *{ grid-column:1 / -1 !important; } }

.an-card{ padding:15px 17px; }
.an-card > .t{ font-size:13px; font-weight:620; letter-spacing:-.008em; }
.an-card > .h{ font-family:var(--mono); font-size:10.5px; color:var(--faint); margin-top:3px;
  margin-bottom:13px; letter-spacing:.04em; }

/* horizontal bar list — labelled, so the sub-3:1 series contrast is discharged */
.barh{ display:flex; flex-direction:column; gap:8px; }
.barh .row{ display:grid; grid-template-columns:130px 1fr auto; align-items:center; gap:11px; }
.barh .lab{ font-size:12.5px; color:var(--fg); font-weight:520; text-align:right;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-family:var(--font); letter-spacing:normal; text-transform:none; }
.barh .track{ height:18px; border-radius:3px; background:var(--s3); overflow:hidden;
  border:1px solid var(--stroke-2); }
.barh .fill{ height:100%; border-radius:2px 0 0 2px; min-width:2px;
  background:var(--bc, var(--accent)); transition:width .5s cubic-bezier(.2,.7,.3,1); }
.barh .val{ font-family:var(--mono); font-size:11.5px; font-weight:600; color:var(--dim);
  font-variant-numeric:tabular-nums; min-width:56px; text-align:right; }
@media (max-width:520px){ .barh .row{ grid-template-columns:96px 1fr auto; } .barh .lab{ font-size:11.5px; } }

/* legend chips for categorical series */
.an-legend{ display:flex; flex-wrap:wrap; gap:6px 14px; margin-top:13px; }
.an-legend .li{ display:inline-flex; align-items:center; gap:6px; font-size:11.5px; color:var(--dim); }
.an-legend .sw{ width:9px; height:9px; border-radius:2px; background:var(--bc,var(--accent)); flex:0 0 auto; }

/* hour × weekday heatmap */
.heat{ display:grid; grid-template-columns:34px repeat(24,1fr); gap:2px; }
.heat .hl{ font-family:var(--mono); font-size:9.5px; color:var(--faint); font-weight:500; display:flex;
  align-items:center; justify-content:flex-end; padding-right:6px; letter-spacing:.04em; text-transform:uppercase; }
.heat .hc{ position:relative; aspect-ratio:1/1; border-radius:2px;
  background:color-mix(in srgb, var(--accent) calc(var(--v,0) * 88%), var(--s3));
  border:1px solid var(--stroke-2); }
.heat .hc[data-v="0"]{ background:var(--s3); }
.heat .hx{ font-family:var(--mono); font-size:9px; color:var(--faint); text-align:center;
  font-variant-numeric:tabular-nums; }
.heat-scale{ display:flex; align-items:center; gap:8px; margin-top:10px; font-family:var(--mono);
  font-size:10px; color:var(--faint); letter-spacing:.06em; text-transform:uppercase; }
.heat-scale .ramp{ height:8px; width:120px; border-radius:2px;
  background:linear-gradient(90deg, var(--s3), var(--accent)); border:1px solid var(--stroke-2); }
@media (max-width:720px){ .heat{ overflow-x:auto; } }

/* line / area trend (inline SVG) */
.spark{ width:100%; height:auto; display:block; overflow:visible; }
.spark .area{ fill:color-mix(in srgb, var(--accent) 14%, transparent); }
.spark .ln{ fill:none; stroke:var(--accent); stroke-width:2; stroke-linejoin:round; stroke-linecap:round; vector-effect:non-scaling-stroke; }
.spark .gl{ stroke:var(--grid); stroke-width:1; vector-effect:non-scaling-stroke; }
.spark .lbl{ fill:var(--faint); font-family:var(--mono); font-size:9.5px; }
.spark .dot{ fill:var(--accent); }

/* plain data-table fallback under every chart (accessibility + the WARN relief) */
.dtable{ width:100%; border-collapse:collapse; font-size:12.5px; font-variant-numeric:tabular-nums; }
.dtable th,.dtable td{ padding:5px 9px; text-align:left; border-bottom:1px solid var(--stroke-2); }
.dtable th{ font-family:var(--mono); color:var(--faint); font-weight:600; font-size:10px;
  letter-spacing:.1em; text-transform:uppercase; }
.dtable td.num,.dtable th.num{ text-align:right; font-family:var(--mono); }
.dtable tbody tr:last-child td{ border-bottom:0; }
.an-table{ margin-top:6px; }
.an-table > summary{ cursor:pointer; font-family:var(--mono); font-size:10px; font-weight:600;
  color:var(--faint); letter-spacing:.12em; text-transform:uppercase; list-style:none; padding:4px 0;
  user-select:none; }
.an-table > summary::-webkit-details-marker{ display:none; }
.an-table > summary::before{ content:"▸ "; }
.an-table[open] > summary::before{ content:"▾ "; }
.an-table > summary:hover{ color:var(--accent); }

/* insufficient-data callout — the honest stand-in for the response-time chart */
.an-insuf{ border:1px dashed color-mix(in srgb,var(--warn) 45%,var(--stroke));
  background:color-mix(in srgb,var(--warn) 6%,var(--s1)); border-radius:var(--r3); padding:15px 17px; }
.an-insuf .t{ font-weight:620; font-size:13px; color:var(--fg); display:flex; align-items:center; gap:8px; }
.an-insuf .b{ font-size:12.5px; color:var(--dim); margin-top:7px; line-height:1.55; }
.an-insuf .samples{ display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
.an-insuf .samples .s{ background:var(--s2); border:1px solid var(--stroke); border-radius:var(--r1);
  padding:6px 10px; font-size:12px; color:var(--dim); }
.an-insuf .samples .s b{ color:var(--fg); font-weight:640; font-family:var(--mono);
  font-variant-numeric:tabular-nums; }

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

/* ===================== print: hand a shift report over ====================
 * Not an afterthought — after-action reviews get printed. Drop the chrome, drop
 * the ink background, keep the data. */
/* ---- the joined unit table ("situation table") --------------------------
 * One row per unit committed to an intervention, whatever system reported it.
 * Shared: the dashboard renders it inside an expanded card, intervencija.php
 * renders the other-sources half of it in its unit rail. See
 * incident_card_lib.php for what fills the cells and why some are empty.
 * -------------------------------------------------------------------- */
/* --- the unit table (fetched) ---------------------------------------- */
/* Two rows per unit: its facts, then its timeline spanning the full width. The
   unit rows carry no bottom rule (the timeline belongs to the row above it) — the
   rule goes under the PAIR, which is what keeps the table reading as one block
   per unit rather than as twice as many rows. */
.utab{ width:100%; border-collapse:collapse; font-size:12px; table-layout:fixed; }
.utab th, .utab td{ text-align:left; padding:7px 9px 5px 0; vertical-align:top; border-bottom:0; }
.utab thead th{ font-family:var(--mono); font-size:8.5px; font-weight:600; letter-spacing:.11em;
  text-transform:uppercase; color:var(--faint); padding-bottom:5px;
  border-bottom:1px solid var(--stroke); }
/* Fixed layout, so the crew pills can never squeeze the SLA columns out of the
   table the way they did when the timeline still had a column of its own. */
.utab thead th:nth-child(1){ width:19%; }
.utab thead th:nth-child(2){ width:34%; }
.utab thead th:nth-child(3){ width:23%; }
.utab thead th:nth-child(4){ width:12%; }
.utab thead th:nth-child(5){ width:12%; }
/* The NAME may be truncated; its qualifiers may not — they sit on their own line
   below it, because inline the ellipsis ate the source badge first. */
.utab tbody th{ font-weight:600; color:var(--fg); }
.utab tbody th > b{ display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.utab tbody th .umeta{ display:flex; align-items:center; flex-wrap:wrap; gap:5px; margin-top:3px; }
.utab tbody th .rid{ font-size:9.5px; color:var(--faint); font-weight:400; }
.utab .num{ text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap;
  padding-right:0; padding-left:9px; }
.utab .num .pt{ color:var(--faint); font-size:10.5px; }
.utab .dim{ color:var(--faint); }
.utab tr.tl > td{ padding:0 0 9px; }
.utab tr.tl{ border-bottom:1px solid var(--stroke-2); }
.utab tbody tr.tl:last-child{ border-bottom:0; }
.utab tr.active > th{ color:var(--fg); }
/* A second (or third) radio on the SAME vehicle — the crew answered on more than one
   handset, which is one unit with several status streams, not several units. Indented
   under its sibling and marked "↳ isto vozilo" in the vehicle cell, so the table can
   keep the per-radio timeline without inflating the apparent commitment. */
.utab tr.u.sameveh > th{ padding-left:11px; font-weight:500; }
.utab tr.u.sameveh > th::before{ content:"↳"; color:var(--faint); margin-right:5px; font-weight:400; }
.utab .crew{ display:flex; flex-wrap:wrap; gap:4px; margin-top:4px; }
/* Every row says which system reported it, because that is what explains the empty
   cells beside it. Small and quiet — it is a qualifier on the name, not a heading. */
.utab tbody th .sb{ font-size:8.5px; padding:1px 5px; flex:0 0 auto; }
/* Rows from a source with no timeline row of their own still need a separator. */
.utab tbody.src tr.u{ border-bottom:1px solid var(--stroke-2); }
.utab tbody.src tr.u:last-child{ border-bottom:0; }
.utab tr.ld td{ color:var(--faint); padding:8px 0; }
.utab-note{ margin-top:7px; font-size:11px; color:var(--faint); line-height:1.45; }

@media (max-width:700px){
    /* Six columns in a phone width is not a comparison; stack the rows. */
    .utab, .utab thead, .utab tbody, .utab tr, .utab th, .utab td{ display:block; width:auto; }
    .utab thead{ display:none; }
    .utab tbody tr{ border-bottom:1px solid var(--stroke); padding:7px 0; }
    .utab th, .utab td{ border-bottom:0; padding:2px 0; }
    .utab .num{ text-align:left; padding-left:0; }
    .utab .num::before{ content:attr(data-l) " "; font-family:var(--mono); font-size:8.5px;
      letter-spacing:.1em; text-transform:uppercase; color:var(--faint); }
}

@media print{
  body{ background:#fff; color:#000; }
  body::before, body::after, .bgfield, .sweep, .rail, header.top, .scrim, #stale-banner{ display:none !important; }
  .shell{ display:block; }
  main{ padding:0; max-width:none; }
  .glass,.card,.kpi,.panel,.iv-card,.ivc{ box-shadow:none; border-color:#bbb; break-inside:avoid; }
  .glass::after,.card::after,.kpi::after,.panel::after,.iv-card::after,.ivc::after,.ticks::before,.kpi::before{ display:none; }
  a{ color:#000; }
  /* Print strips backgrounds, so the rails' state has to survive as GEOMETRY:
     a filled dot is a phase that happened, a hollow one is a phase that did not. */
  .cr-n .cr-d{ border-color:#000; background:#fff; }
  .cr-n.done .cr-d, .cr-n.now .cr-d{ background:#000; }
  .cr-n + .cr-n::before{ background:#ccc; }
  .cr-n.done::before, .cr-n.now::before{ background:#000; }
  .srail .seg::before{ background:#000; }
  .srail .seg.pending::before{ background:#ccc; }
}
