:root {
  --bg: #000;
  --panel: #080a0c;
  --line: #1b2129;
  --ink: #c9d4df;
  --ink-dim: #63707d;

  --boundary: #35e08d;
  --structure: #7fd4ff;
  --structure-fill: rgba(127, 212, 255, .10);
  --road: #1e242b;
  --detail: #4a6b80;
  --road-edge: #2c343d;
  --parcel: #1a2029;
  --osm: #2f6f8f;
  --ms: #8a6a25;
  --camera: #ffb020;
  --live: #ff4d6d;
  --veh-line: #7fd3ff;
  --veh-dark: #1b3a4d;
  --detect: #b18cff;
  --measure: #ff5c8a;
  --hyp: #c79bff;
  --grid: #0d1116;
}

* { box-sizing: border-box; }

html, body {
  margin: 0; height: 100%; overflow: hidden;
  background: var(--bg); color: var(--ink);
  font: 13px/1.5 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  -webkit-font-smoothing: antialiased;
}

#app { display: flex; height: 100%; }

/* --- map ------------------------------------------------------------------ */

#stage { flex: 1; position: relative; overflow: hidden; cursor: grab; background: #000; }
#stage.panning { cursor: grabbing; }
#stage.measuring { cursor: crosshair; }
#map { width: 100%; height: 100%; display: block; }

/* --- 3D view -------------------------------------------------------------- */
/* Fills the same stage as the SVG map; the two are shown one at a time. The
   canvas is the drawing surface, #labels3d is the HTML overlay that carries the
   screen-anchored name labels (same convention as #labels for the 2D map). */
#stage3d { position: absolute; inset: 0; }
#stage3d.hidden { display: none; }
#canvas3d { width: 100%; height: 100%; display: block; cursor: grab; }
#canvas3d:active { cursor: grabbing; }
#labels3d { position: absolute; inset: 0; pointer-events: none; }
#labels3d span {
  position: absolute; transform: translate(-50%, -50%); white-space: nowrap;
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  text-shadow: 0 0 5px #000, 0 0 5px #000, 0 0 3px #000;
}
#labels3d .l-structure3d { color: #9fdfff; }
#labels3d .l-camera3d    { color: var(--camera); letter-spacing: .1em; }
#labels3d .l-vehicle3d   { color: #ffd75e; }

.grid      { stroke: var(--grid); stroke-width: 1px; vector-effect: non-scaling-stroke; }
.grid.axis { stroke: #17202a; }

.road      { fill: none; stroke: var(--road); stroke-linecap: round; stroke-linejoin: round;
             vector-effect: non-scaling-stroke; }
.road-edge { fill: none; stroke: var(--road-edge); stroke-width: 1px;
             stroke-linecap: round; vector-effect: non-scaling-stroke; }

.parcel { fill: none; stroke: var(--parcel); stroke-width: 1px;
          vector-effect: non-scaling-stroke; }

.boundary {
  fill: rgba(53, 224, 141, .045); stroke: var(--boundary); stroke-width: 2px;
  stroke-linejoin: round; vector-effect: non-scaling-stroke;
}
.corner { fill: var(--boundary); }

.structure {
  fill: var(--structure-fill); stroke: var(--structure); stroke-width: 1.5px;
  stroke-linejoin: round; vector-effect: non-scaling-stroke;
}
.structure:hover { fill: rgba(127, 212, 255, .22); }

.bldg      { stroke-width: 1px; vector-effect: non-scaling-stroke; fill: none;
             stroke-dasharray: 4 3; }
.bldg.osm  { stroke: var(--osm); }
.bldg.ms   { stroke: var(--ms); }

.cam-fov   { fill: rgba(255, 176, 32, .10); stroke: rgba(255, 176, 32, .45);
             stroke-width: 1px; vector-effect: non-scaling-stroke; }
.cam-dot   { fill: var(--camera); stroke: #000; stroke-width: 1.5px;
             vector-effect: non-scaling-stroke; }
.cam-dot.ptz { fill: none; stroke: var(--camera); stroke-width: 2px; }

.measure-line { stroke: var(--measure); stroke-width: 1.5px; stroke-dasharray: 5 4;
                vector-effect: non-scaling-stroke; }
.measure-dot  { fill: var(--measure); }

.hidden { display: none; }

/* --- map labels ----------------------------------------------------------- */

#labels { position: absolute; inset: 0; pointer-events: none; }
#labels span {
  position: absolute; transform: translate(-50%, -50%); white-space: nowrap;
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  text-shadow: 0 0 5px #000, 0 0 5px #000, 0 0 3px #000;
}
#labels .l-structure { color: #9fdfff; }
#labels .l-camera    { color: var(--camera); letter-spacing: .1em; }
#labels .l-road      { color: #4a5763; }

/* --- overlays ------------------------------------------------------------- */

.overlay {
  position: absolute; background: rgba(0, 0, 0, .72); border: 1px solid var(--line);
  border-radius: 3px; padding: 8px 10px; pointer-events: none;
}

#scalebar { left: 16px; bottom: 16px; }
#scalebar .bar { height: 5px; border: 1px solid var(--ink); border-top: none; margin-bottom: 4px; }
#scalebar .label { text-align: center; color: var(--ink-dim); font-size: 11px; }

#view-note { right: 72px; bottom: 16px; max-width: 320px; font-size: 11px;
             color: var(--ink-dim); line-height: 1.45; }

/* Every other overlay is decoration and sets pointer-events:none; these are controls, so
   they take clicks and sit above the map surface that would otherwise swallow them. */
#views { right: 16px; bottom: 62px; width: 44px; display: flex; flex-direction: column;
         gap: 4px; padding: 4px; pointer-events: auto; z-index: 5; }
#views button {
  font: inherit; font-size: 10px; letter-spacing: .08em; padding: 4px 0;
  background: transparent; color: var(--ink-dim); border: 1px solid var(--ink-dim);
  border-radius: 2px; cursor: pointer;
}
#views button:hover { color: var(--ink); border-color: var(--ink); }
#views button.on { color: var(--bg); background: var(--ink); border-color: var(--ink); }
#views button.unbuilt { opacity: .55; }

#compass { right: 16px; bottom: 16px; text-align: center; width: 44px; }
#compass .needle { font-size: 14px; line-height: 1; color: var(--boundary); }
#compass .n { font-size: 10px; color: var(--ink-dim); letter-spacing: .1em; }

#readout { left: 16px; top: 16px; font-size: 11px; color: var(--ink-dim); }
#readout b { color: var(--ink); font-weight: 500; }

/* --- side panel ----------------------------------------------------------- */

#panel {
  width: 320px; flex: none; background: var(--panel); border-left: 1px solid var(--line);
  padding: 20px; overflow-y: auto;
}
#panel h1 { font-size: 15px; margin: 0; font-weight: 600; letter-spacing: -.01em; }
#panel h2 {
  font-size: 10px; text-transform: uppercase; letter-spacing: .14em;
  color: var(--ink-dim); margin: 22px 0 9px; font-weight: 600;
}
#panel h2:first-of-type { margin-top: 0; }


.layer { display: flex; align-items: center; gap: 9px; padding: 4px 0; cursor: pointer; }
.layer input { accent-color: var(--boundary); margin: 0; }
.swatch { width: 11px; height: 11px; border-radius: 2px; border: 1.5px solid; flex: none; }
.layer .count { margin-left: auto; color: var(--ink-dim); font-size: 11px;
                font-variant-numeric: tabular-nums; }

button {
  font: inherit; color: var(--ink); background: #12171d; border: 1px solid var(--line);
  border-radius: 3px; padding: 6px 11px; cursor: pointer;
}
button:hover { border-color: var(--ink-dim); }
button[aria-pressed="true"] { background: var(--measure); border-color: var(--measure); color: #14060d; }


/* The hover/selection readout, now an overlay on the map. It has to disappear when it
   has nothing to say, or an empty box sits over the property permanently -- :empty does
   that without any JS needing to toggle a class. */
#selected {
  left: 16px; top: 56px; max-width: 300px; font-size: 11.5px; color: var(--ink-dim);
  line-height: 1.5; z-index: 4;
}
#selected:empty { display: none; }
#selected .name { color: var(--ink); display: block; margin-bottom: 4px; }
#selected .row { display: flex; justify-content: space-between; gap: 12px; }
#selected img { width: 100%; border: 1px solid var(--line); border-radius: 2px;
                margin-top: 8px; display: block; }


.detail { fill: none; stroke: var(--detail); stroke-width: 1px;
          vector-effect: non-scaling-stroke; }

.cam-fov.placing { fill: rgba(255,176,32,.20); stroke: var(--camera); stroke-dasharray: 4 3; }
.cam-dot.placing { fill: #fff; }

  font: inherit; color: var(--ink); background: #12171d;
  border: 1px solid var(--line); border-radius: 3px; padding: 5px 6px;
}

.obj        { fill: var(--live); stroke: #000; stroke-width: 1px;
              vector-effect: non-scaling-stroke; }
.obj.vision { fill: var(--detect); }
.obj-box    { fill: var(--live); fill-opacity: .18; stroke: var(--live);
              stroke-width: 1.5px; vector-effect: non-scaling-stroke; }
.obj-box.vision { stroke: var(--detect); fill: var(--detect); fill-opacity: .12; }
.obj-box.suspect { fill-opacity: 0; stroke-dasharray: 4 3; }
.obj-box.triangulated { stroke-width: 2.5px; fill-opacity: .25; }
/* The vehicle outline a detection stands for: factory length by width, at the angle its
   silhouette implies. Dashed because that angle is inferred from one view, not measured
   - the same convention the measured footprints use for an assumed dimension. */
.obj-foot   { fill: var(--live); fill-opacity: .18; stroke: var(--live);
              stroke-width: 1.5px; stroke-dasharray: 5 3;
              vector-effect: non-scaling-stroke; }
.obj-foot.vision { stroke: var(--detect); fill: var(--detect); fill-opacity: .12; }
.obj-foot.suspect { fill-opacity: 0; stroke-dasharray: 4 3; }
.obj-foot.triangulated { stroke-width: 2.5px; fill-opacity: .25; }
/* No known vehicle matched, so there is no outline to draw. This is the one thing that
   was actually measured - the silhouette across the sightline - drawn at its true width
   with no depth invented behind it. */
.obj-sil    { fill: none; stroke: var(--live); stroke-width: 3px;
              stroke-linecap: round; vector-effect: non-scaling-stroke; }
.obj-sil.vision { stroke: var(--detect); }
.obj-sil.suspect { stroke-dasharray: 4 3; }
.obj-ray    { stroke: var(--live); stroke-width: 1px; stroke-dasharray: 3 3;
              opacity: .35; vector-effect: non-scaling-stroke; fill: none; }
.obj-ray.vision { stroke: var(--detect); }
#labels .l-object { color: var(--live); }
#labels .l-object.vision { color: var(--detect); }

/* Where a live object has been. Drawn the way the survey draws a fence: flat colour,
   one crisp width, fully opaque. A translucent line reads as a smudge over the map and
   picks up whatever it crosses; a solid one is a line. Slightly heavier than the
   detail linework so a path is the thing the eye lands on. */
.obj-trail        { fill: none; stroke: var(--live); stroke-width: 2.5px;
                    stroke-linecap: round; stroke-linejoin: round;
                    vector-effect: non-scaling-stroke; }
.obj-trail.vision { stroke: var(--detect); }

/* Little solids for the parked vehicles: a measured footprint, extruded by the measured
   height. Drawn as a plan with the height thrown "north" up the screen - not a real
   projection, but it reads as a solid at a glance and keeps the footprint honest,
   because the base of the box is exactly where the survey frame says the vehicle is. */
.veh-base { fill: var(--veh-dark); stroke: var(--veh-line); stroke-width: 1.5px;
           vector-effect: non-scaling-stroke; fill-opacity: 0.42; }
/* The nose tick: which way the vehicle points, readable without hovering. */
.veh-nose { stroke: var(--veh-line); stroke-width: 1.5px; opacity: 0.85;
            vector-effect: non-scaling-stroke; }
/* A dimension the fit could not measure is drawn dashed, so a factory figure or a
   class default never reads as something this map worked out. */
.veh-assumed .veh-base { stroke-dasharray: 4 3; }
.veh-wheel { fill: var(--veh-line); stroke: none; }
#labels .l-veh { color: var(--veh-line); font-weight: 600; }

/* One real object, as opposed to one camera's opinion of one. The per-camera dots stay
   drawn underneath it: the ring says "these are the same person" without hiding what
   each camera actually reported, which is still the evidence a pose is wrong. */
.ident       { fill: none; stroke: var(--live); stroke-width: 2px;
               vector-effect: non-scaling-stroke; }
/* Nobody can see it right now, but it has not stopped existing - it is between
   cameras. Dashed and faded, because this position is dead reckoning, not a sighting. */
.ident.coasting { stroke-dasharray: 3 3; stroke-opacity: .45; }
.ident-arrow { stroke: var(--live); stroke-width: 2px; fill: none;
               stroke-linecap: round; stroke-linejoin: round;
               vector-effect: non-scaling-stroke; }
.ident-arrow.coasting { stroke-opacity: .45; }
#labels .l-ident { color: var(--live); font-weight: 600; }

/* Where an object went while nothing could see it: from its last real sighting to where
   dead reckoning puts it now. Drawn faint and dashed, because it is inference. */
.ident-lead { fill: none; stroke: var(--live); stroke-width: 2px; stroke-dasharray: 2 4;
              stroke-opacity: .5; vector-effect: non-scaling-stroke; }

/* A fix the geometry does not believe: drawn, because the camera did see something,
   but hollow and dashed so it never reads as a known position. */
.obj.suspect { fill: none; stroke: var(--live); stroke-width: 2px;
               stroke-dasharray: 2 2; vector-effect: non-scaling-stroke; }
.obj.suspect.vision { stroke: var(--detect); }

/* Paths already finished. Same solid line as a live trail, a touch thinner so history
   never competes with what is happening now; each ends where tracking was lost. */
/* An unplaced camera. A dashed square, never the solid dot that means a camera is
   somewhere: the shape has to differ from a located camera at a glance, because the
   marker sits at an arbitrary spot in a holding strip and must not be read as a fix. */
.cam-unplaced {
  fill: none; stroke: var(--ink-dim); stroke-width: 1.5px;
  stroke-dasharray: 3 2; vector-effect: non-scaling-stroke;
}
.cam-unplaced:hover { stroke: var(--measure); }
#labels .l-unplaced { color: var(--ink-dim); letter-spacing: .1em; }

.track-line { fill: none; stroke: var(--live); stroke-width: 2px;
              stroke-linecap: round; stroke-linejoin: round;
              vector-effect: non-scaling-stroke; }
.track-end  { fill: none; stroke: var(--live); stroke-width: 2px;
              vector-effect: non-scaling-stroke; }
/* The object's own snapshot and clip, in the hover panel. Capped in height because the
   panel is an overlay on the map and must not grow to cover what it is describing. */
.obj-crop { width: 100%; max-height: 150px; object-fit: cover; border: 1px solid var(--line);
            border-radius: 2px; margin: 6px 0 4px; display: block; }
.obj-clip { width: 100%; max-height: 220px; margin: 6px 0; border-radius: 2px; display: block; }
.obj-play { width: 100%; margin: 6px 0 2px; cursor: pointer; }
#selected .dim { opacity: .6; font-size: 10.5px; line-height: 1.4; }
#selected { pointer-events: auto; max-height: 70vh; overflow-y: auto; }

#tracking input[type=range] { width: 100%; margin: 8px 0 0; display: block; }
#track-axis {
  display: flex; justify-content: space-between; font-size: 10px;
  color: var(--ink-dim); margin-top: 2px; font-variant-numeric: tabular-nums;
}
#track-label { color: var(--live); font-size: 11.5px; margin: 6px 0 8px; }
#track-label b { font-weight: 500; font-variant-numeric: tabular-nums; }
#track-status { font-size: 11.5px; color: var(--ink-dim); line-height: 1.5; margin-top: 8px; }
#track-status b { color: var(--ink); font-weight: 500; }

/* The path timeline. Linear in time, so the axis under it can be evenly spaced and
   mean something: the middle label really is half the history of the right-hand one.
   The handle grows to the right, like every other slider. */
#tracks-controls { display: block; margin: 6px 0 8px; }
#tracks-controls input[type=range] { width: 100%; margin: 0; display: block; }
#tracks-axis {
  display: flex; justify-content: space-between; gap: 6px;
  font-size: 10px; color: var(--ink-dim); margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
#tracks-axis span:nth-child(2) { opacity: .65; }
#tracks-axis span:last-child { text-align: right; }
#tracks-window-label { color: var(--live); font-size: 11.5px; margin-top: 7px; }
/* The window length. Buttons rather than a second slider: there are five useful values
   and a slider for five stops is harder to hit than five targets. */
#tracks-span { display: flex; gap: 4px; margin-top: 7px; }
#tracks-span button { flex: 1; padding: 3px 0; font-size: 10.5px; }
#tracks-span button.on { border-color: var(--live); color: var(--live); }
#tracks-window-label b { font-weight: 500; font-variant-numeric: tabular-nums; }
#tracks-status { font-size: 11.5px; color: var(--ink-dim); line-height: 1.5; }
#tracks-status b { color: var(--ink); font-weight: 500; }

/* The 3D-only layer switches sit under the shared ones with a hairline between, so it
   is visible that they belong to the same list but only bite in one view. */
#layers3d { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }

/* Vehicle sprites: pixel art, so never let the browser smooth it. Scaling is done in
   metres by the viewBox, which is what keeps one sprite pixel a fixed size of ground. */
.veh-sprite { image-rendering: pixelated; image-rendering: crisp-edges; }

/* A vehicle that was found and placed but whose size is not known: no roster entry and
   nothing measured. Drawn as a cross-hair marker rather than a footprint, because the
   position is real and the extent is not. */
.veh-unsized circle { fill: none; stroke: var(--detect); stroke-width: 1.5px;
                      vector-effect: non-scaling-stroke; }
.veh-unsized line   { stroke: var(--detect); stroke-width: 1.5px;
                      vector-effect: non-scaling-stroke; }

/* Anchors: permanent reference objects, generated by build_anchors.py.
   A dashed outline means the dimension behind it is a STANDARD, not a measurement -
   so a shape you can trust and a placeholder never look alike. */
.anchor-canopy { fill: rgba(110, 200, 130, 0.10); stroke: #6ec882; stroke-width: 0.12; }
.anchor-canopy.standard { stroke-dasharray: 0.6 0.4; }
.anchor-trunk { fill: #6b4a2f; stroke: none; }
.anchor-lift-bar { stroke: #4d7cff; stroke-width: 0.14; }
.anchor-post { fill: #4d7cff; stroke: none; }
.anchor-post-dot { fill: #9aa4b2; stroke: none; }
.anchor:hover .anchor-canopy { fill: rgba(110, 200, 130, 0.22); }
.l-anchor { fill: #8fd6a0; font-size: 0.9px; }
/* A provisional anchor has a position but nothing confirming it. It is drawn, because
   hiding a weak fix is as dishonest as drawing it confidently - but never like a solid one. */
.anchor.provisional { opacity: 0.55; }
.anchor.provisional .anchor-canopy { stroke-dasharray: 0.3 0.5; }
.anchor.provisional .anchor-trunk { fill: #8a7a5f; }
.row.warn span:last-child { color: #e0a33c; }


/* Clues: what a camera reads off its own frame. Drawn where the camera believes the thing
   is, so a clue lying along a surveyed line is agreement and one floating in open ground
   is the camera being wrong - both worth seeing. */
:root { --clue: #7fd1ff; }
.clue-line { stroke: var(--clue); stroke-width: 2px; vector-effect: non-scaling-stroke;
             opacity: 0.9; }
.clue-corner { stroke: var(--clue); stroke-width: 2.5px; vector-effect: non-scaling-stroke;
               opacity: 1; }
#g-clues:hover .clue-line { opacity: 1; }

/* 3D shares the survey palette and keeps its controls in the existing panel. */
#map.hidden, [hidden] { display: none !important; }
#stage { min-width: 0; }
#canvas3d { touch-action: none; }
.view-3d #scalebar { display: none; }
#labels3d span { max-width: 260px; overflow: hidden; text-overflow: ellipsis; }
#labels3d .l-anchor3d { color: #8fd6a0; }
#controls3d { margin-top: 22px; }
#controls3d p { margin: 8px 0; }
#controls3d > label[for] { display: block; margin: 14px 0 6px; }
#focus3d { width: 100%; min-width: 0; padding: 8px; font: inherit; color: var(--ink); background: #12171d; border: 1px solid var(--line); border-radius: 3px; }
.layer:has(input:disabled) { opacity: .45; cursor: default; }
button:disabled { opacity: .45; cursor: default; }
button:focus-visible, select:focus-visible, input:focus-visible { outline: 2px solid var(--boundary); outline-offset: 3px; }
#selected .row span { overflow-wrap: anywhere; min-width: 0; }
#view-note { color: #aebbc7; }
#views button { min-height: 32px; }
@media (max-width: 700px) {
  #app { flex-direction: column; }
  #stage { flex: 1 1 58%; min-height: 280px; }
  #panel { width: 100%; flex: 0 1 42%; min-height: 0; padding: 16px; border-left: 0; border-top: 1px solid var(--line); }
  #panel h1 { font-size: 16px; }
  #focus3d { font-size: 16px; }
  #controls3d button, #views button { min-height: 40px; }
  #labels3d span { font-size: 9px; max-width: 160px; }
  #view-note { left: 12px; right: 72px; max-width: none; bottom: 12px; }
}

#vehicle-detection { margin: 20px 0; }
#vehicle-detection p { margin: 8px 0; font-size: 12px; color: #94a3b1; }
#vehicle-scan-status { overflow-wrap: anywhere; }
#vehicle-unplaced { font-size: 12px; color: #b6c3ce; }
#vehicle-unplaced summary { cursor: pointer; }
#vehicle-unplaced ul { padding-left: 18px; }
#vehicle-unplaced li { margin: 6px 0; }

#labels3d .camera-label-leader {
  position: absolute; width: 1px; background: var(--camera); opacity: .55;
}


/* --- erratic behaviour ---------------------------------------------------- */
/* Density matters here more than anywhere else on the page: a tuning session means
   reading dozens of flags and judging each one, so a row has to show its camera, its
   time, and the numbers that raised it without being opened. */
#erratic-controls { display: flex; flex-direction: column; gap: .4rem; }
.erratic-row { display: flex; gap: .5rem; }
.erratic-row label { flex: 1; display: flex; flex-direction: column;
                     font-size: .75rem; opacity: .8; gap: .15rem; }
.erratic-row input { width: 100%; box-sizing: border-box; }
.erratic-slider { display: block; font-size: .75rem; opacity: .85; }
.erratic-slider input { width: 100%; }
#erratic-run { margin-top: .2rem; padding: .35rem; cursor: pointer; }
#erratic-run[disabled] { opacity: .5; cursor: progress; }
#erratic-status { font-size: .75rem; opacity: .8; margin: .4rem 0; }
#erratic-list { display: flex; flex-direction: column; gap: .35rem;
                max-height: 22rem; overflow-y: auto; }
.erratic-inc { border-left: 3px solid var(--warn, #d08a2c); padding: .3rem .45rem;
               background: rgba(255, 255, 255, .04); font-size: .78rem; }
.erratic-inc.v-true  { border-left-color: #c0392b; }
.erratic-inc.v-false { border-left-color: #555; opacity: .55; }
.erratic-inc.v-unsure{ border-left-color: #7d8fa6; }
.erratic-inc h3 { margin: 0 0 .15rem; font-size: .8rem; font-weight: 600; }
.erratic-cams { opacity: .8; font-size: .72rem; }
.erratic-z { font-family: ui-monospace, monospace; font-size: .7rem; opacity: .85;
             display: block; margin: .15rem 0; white-space: pre-wrap; }
.erratic-verdict { display: flex; gap: .25rem; margin-top: .25rem; }
.erratic-verdict button { font-size: .68rem; padding: .1rem .4rem; cursor: pointer; }
.erratic-verdict button[aria-pressed="true"] { outline: 2px solid currentColor; }
#erratic-list .none { opacity: .7; font-size: .75rem; }

/* Placement hypotheses (debug). Everything dashed or hollow: none of it is a fix.
   A cloud dot is one solution the solver kept; the ring is their median; the region
   outline is where the owner says the coop is. */
.hyp-region { fill: none; stroke: var(--hyp); stroke-width: 1px; stroke-dasharray: 6 4;
              vector-effect: non-scaling-stroke; opacity: .6; }
.hyp-cloud  { fill: var(--hyp); opacity: .35; }
.hyp-ocloud { fill: var(--camera); opacity: .25; }
.hyp-cam    { fill: none; stroke: var(--hyp); stroke-width: 2px; stroke-dasharray: 3 2;
              vector-effect: non-scaling-stroke; }
.hyp-fan    { stroke: var(--hyp); stroke-width: 1px; stroke-dasharray: 4 3;
              vector-effect: non-scaling-stroke; }
.hyp-obj    { fill: none; stroke: var(--camera); stroke-width: 2px;
              vector-effect: non-scaling-stroke; }
.hyp-cam:hover, .hyp-obj:hover, .hyp-region:hover { stroke: var(--measure); }
#labels .l-hyp { color: var(--hyp); }
#labels .l-hypobj { color: var(--camera); }

/* One-off anchors: one of a kind on the property. A one-off with a MEASURED footprint is
   drawn as that footprint - true size, true angle - and the diamond is the fallback for
   one whose shape is still unknown, sized by how many cameras see it. The two are never
   drawn together: the diamond is 2.10 m across at four cameras and swallowed the small
   brown house's 0.87 x 1.20 m rectangle whole. */
.anchor-oneoff { fill: #8fd6a0; fill-opacity: .55; stroke: #8fd6a0; stroke-width: 1.5px;
                 vector-effect: non-scaling-stroke; }
.anchor-oneoff.alignment { fill: none; stroke-dasharray: 3 2; }
.anchor-oneoff-footprint { fill: none; stroke: #8fd6a0; stroke-width: 1px; stroke-dasharray: 5 3;
                           vector-effect: non-scaling-stroke; opacity: .7; }
/* Measured: solid, because this is the object and not an annotation about it. */
.anchor-oneoff-footprint.measured { fill: #8fd6a0; fill-opacity: .55; stroke-width: 1.5px;
                                    stroke-dasharray: none; opacity: 1; }
.anchor-oneoff-footprint.measured.alignment { fill: none; stroke-dasharray: 3 2; }
.anchor-oneoff-point { fill: #08110c; stroke: #8fd6a0; stroke-width: 1px;
                       vector-effect: non-scaling-stroke; }
.anchor:hover .anchor-oneoff,
.anchor:hover .anchor-oneoff-footprint.measured { stroke: var(--measure); }

/* The declutter pass reports what it could not fit, beside the scale bar - crowding is a
   fact about the zoom, and this is where the other zoom facts already are. */
#labels-note { color: var(--ink-dim); font-size: 10px; letter-spacing: .06em; }

/* Satellite base layer: a black-and-white, dimmed backdrop so the drawn geometry reads
   over it. view3d.js applies the same filter (IMAGERY_FILTER) to the 3D ground. */
#g-imagery { filter: grayscale(1) brightness(0.55) contrast(1.15); }

/* Readout rows: the label column keeps its width; the value wraps. Without this a long
   value squeezed "Who" into one letter per line. */
#selected .row > span:first-child { flex: 0 0 auto; max-width: 40%; }

/* Drone clip timeline */
#timeline { display: grid; gap: 6px; margin-bottom: 8px; }
#timeline .tl-row { display: flex; align-items: center; gap: 8px; }
#timeline input[type=range] { flex: 1; }
#timeline output { flex: 1; font-variant-numeric: tabular-nums; font-size: 11px; }
#timeline button, #timeline select { font: inherit; font-size: 11px; background: transparent; color: var(--ink);
  border: 1px solid var(--line); border-radius: 3px; padding: 2px 8px; cursor: pointer; }

/* --- embed (?embed) ---------------------------------------------------------- */
/* A small square iframe on the company site: the map fills it, the panel and the debug
   view are left to the full page, which the corner link opens. */
#embed-open { display: none; }
.embed #panel, .embed #views [data-view="debug"] { display: none; }
.embed #stage { min-height: 0; }
.embed #embed-open {
  display: block; left: 12px; top: 12px; z-index: 6; pointer-events: auto;
  font-size: 11px; color: var(--ink); text-decoration: none;
}
/* The hover card (photo, height, source) covers most of a 600 px tile. */
.embed #readout, .embed #selected { display: none !important; }
