﻿.no-screen, .no-screen * {
    display: none !important;
}

/* Composition frame for stacked Icon glyphs (DetectionTypeIcon's object base + event marker).
   Mirrors Font Awesome's .fa-layers geometry — a 1em box with each glyph absolutely centered, its
   own Transform (translate/scale, set inline by Icon.razor) offsetting it from center — but owned
   by us: fa-layers CSS is injected at runtime by all.min.js, and the Icon primitive no longer
   depends on that runtime for cataloged glyphs. */
.icon-layers {
    display: inline-block;
    height: 1em;
    width: 1em;
    position: relative;
    text-align: center;
    vertical-align: -0.125em;
}

/* svg only. This used to match `> i` as well, for glyphs that fell back to the Font Awesome <i> tag;
   there is no fallback and no Font Awesome, so that half had nothing left to match.
   No vertical-align here either: baseline alignment for the stack is owned by .icon-layers above, and
   on an absolutely-positioned child the property does nothing — it was also being overridden by the
   inline vertical-align Icon.razor writes into the style attribute, so it never applied twice over. */
.icon-layers > svg {
    position: absolute;
    inset: 0;
    margin: auto;
    /* Center-out: each glyph's inline Transform offsets it from its own centre. */
    transform-origin: center center;
}

.detection-display {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 53px;
}

/* DO NOT add `contain` here. It is tempting — this row is the unit the virtualized grid rebuilds — but
   layout containment makes an element a containing block for position: fixed descendants, and the
   expanded-detection overlay (SharedKernelModule OverlayComponent -> MudOverlay) renders INLINE as a
   sibling of the card inside this row rather than being portaled to the popover provider. Containing the
   row would confine that full-screen overlay to one 270px row.

   Containment on the CARD was tried too and measured as worthless: no change to the browser
   layout/paint time (~156ms before, ~168ms after, for 36 cards), with getComputedStyle confirming the
   rule was live. That makes sense in hindsight — containment stops invalidation from propagating out of
   an element, and every scroll replaces the whole card window at once, so there is no small mutation to
   contain. Don't re-try it without a different reason. */
.detection-grid-row {
    display: grid;
    column-gap: 16px;
    row-gap: 32px;
    padding: 16px 20px;
    box-sizing: border-box;
}

.detection-grid-card {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: 8px;
    transition: box-shadow 120ms ease;
}

/* The black drop shadow reads the hover in LIGHT mode but all but disappears on the dark canvas, where
   the card and the page sit close in brightness. The redesign added a hairline ring in
   --mud-palette-lines-inputs to carry it in dark mode, but that divider colour is too faint to notice.

   The ring is now drawn from --mud-palette-text-primary, which FLIPS with the theme: a dark ring that
   reinforces the shadow in light mode, and a light ring that clearly stands off the dark card in dark
   mode — so the hover is equally legible in both, with nothing in CSS having to know which mode is
   active (there is no dark-mode class here, only the swapped palette variables). Mixed toward
   transparent and set to 2px so it reads as a deliberate highlight rather than a hard outline or one of
   the hairline dividers already on the page. box-shadow is exempt from the theme-colour convention, so
   the black shadow layer stays a literal. */
.detection-grid-card:hover {
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25),
                0 0 0 2px color-mix(in srgb, var(--mud-palette-text-primary) 60%, transparent);
}

.detection-grid-card-image {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    background-color: var(--mud-palette-action-default-hover);
    border-radius: 8px;
    overflow: hidden;
    cursor: pointer;
}

/* Blurred fill for the letterbox/pillarbox bars the contained crop leaves behind — a dimmed,
   blown-up copy of the tile's own image instead of a flat plate, so the bars carry the colour of
   the thing they surround. The source arrives as --tile-fill from DetectionComponent (same URL as
   the crop, so nothing extra is fetched); with the property unset the declaration is invalid at
   computed-value time and no fill paints, leaving the flat background above.

   scale() overshoots deliberately: blur samples past the edges of its own box and fades out there,
   which would draw a soft frame inside the tile — overscanning pushes that fade outside the
   overflow:hidden clip. The crop itself is given position + z-index at the call site so it paints
   over this; the corner icon sits above both at z-index 2. */
.detection-grid-card-image::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image: var(--tile-fill);
    background-position: center;
    background-size: cover;
    filter: blur(12px);
    transform: scale(1.18);
    opacity: 0.5;
    pointer-events: none;
}

.detection-grid-card-details {
    flex: 0 0 auto;
    display: grid;
    padding: 1px 2px 1px;
    min-width: 0;
    gap: 2px;
    border-radius: 0 0 8px 8px;
}

/* Metadata ABOVE the image (InfoPlacement Above). The band is still rendered after the image in the
   DOM, but the card is a flex column, so order:-1 lifts it to the top. The rounded corners swap to the
   top so the band caps the card cleanly. */
.detection-grid-card-details-above {
    order: -1;
    border-radius: 8px 8px 0 0;
}

/* Timestamp (left) + device name (right) on a single row. Both truncate with
   ellipses rather than wrapping. Timestamp keeps its natural width; the device
   name flexes into the remaining space and truncates first when things get tight. */
.detection-grid-card-header-line {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    cursor: pointer;
}

.detection-grid-card-timestamp {
    flex: 0 0 auto;
    font-size: 0.7rem;
    opacity: 0.75;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.detection-grid-card-device-name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.7rem;
    opacity: 0.75;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: right;
}

/* In the grid card context only: truncate the DetectionDetailsComponent's detection-text
   MudText (rendered as a <p class="mud-typography …">) instead of wrapping. MudStack doesn't
   use a .mud-stack class — it renders utility classes like .d-flex / .flex-row — so we
   apply min-width: 0 to every descendant to let the flex chain shrink the text below its
   nowrap min-content. Scoped to .detection-grid-card-details so list view is unaffected. */
.detection-grid-card-details * {
    min-width: 0;
}

.detection-grid-card-details .mud-typography {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Grid-view only: shrink the detection icon and magnifying-glass icon-button one step
   below MudBlazor's Size.Small defaults (1.25rem / 1.125rem). Scoped to the grid card so
   list view sizing is untouched. Covers both the in-details icon and the corner-icon
   overlay that appears when there is no detection text. */
.detection-grid-card-details .mud-icon-size-small,
.detection-grid-card-corner-icon .mud-icon-size-small {
    font-size: 1rem;
}

.detection-grid-card-details .mud-icon-button-size-small {
    padding: 2px;
    font-size: 0.9rem;
}

/* The coloured disc a detection mark is drawn on, wherever it appears — the grid tile's corner
   overlay AND the list row. The glyph is often drawn straight onto a photo, so it needs its own
   ground to stay legible, and the colour of that ground identifies the DETECTION TYPE: every face
   detection is one colour, every plate read another. DetectionComponent / DetectionDetailsComponent
   set --mark-accent from DetectionTypeProfiles.

   The accent is a single MODE-INDEPENDENT hue and the two colours below are mixed from it here,
   against --mud-palette-* values that MudThemeProvider regenerates on the light/dark flip. So the
   plate tints toward whatever surface it is on and the ink toward whatever the text colour is, and
   nothing in C# has to know which mode is active — which is load-bearing, because
   DetectionComponentModel.IsDarkMode is never actually assigned, so a C#-side light/dark choice
   silently rendered the light variant in both modes. */
.detection-mark-plate {
    display: flex;
    align-items: center;
    justify-content: center;
    /* A FIXED SQUARE, not padding around the glyph — that is what makes every plate the same
       circle. Glyph boxes are not square and not even consistently oriented: all the marks are
       1em tall, but Truck/Bicycle/Motorcycle/Boat are 640x512 (1.25em wide, a wide oval under
       even padding) while Bus and Bag are 448x512 and Person and Train 384x512 (0.75-0.875em, a
       TALL oval). Padding preserves whatever shape the glyph box has; a fixed square cannot.
       2rem holds the widest mark - 1.25em at the Small font-size of 1.25rem is 25px inside a 32px
       circle - and the glyphs are silhouettes whose ink does not reach their box corners. */
    width: 2rem;
    height: 2rem;
    flex: 0 0 auto;
    border-radius: 50%;
    /* --mark-plate-strength is the user's opacity setting, inherited from the cards container so one
       declaration reaches every disc in both list and grid (DetectionsPage). The 22% fallback is the
       reviewed default, used anywhere the property is not in scope — the detection overlay's header and
       the Cases page, which have no card settings of their own. */
    background-color: color-mix(in srgb, var(--mark-accent, transparent) var(--mark-plate-strength, 22%), var(--mud-palette-surface));
    color: color-mix(in srgb, var(--mark-accent, var(--mud-palette-text-primary)) 60%, var(--mud-palette-text-primary));
    /* A hairline separation from a photo that happens to match the plate. Rendered inside
       so it costs no layout space and cannot shift the glyph off its inset. */
    box-shadow: inset 0 0 0 1px rgb(0 0 0 / 12%);
    /* User mark-size control (MarkSize). One transform scales the WHOLE plate — the disc and its glyph in
       icon mode, the pill and its label in text mode — so a single knob sizes the mark whichever form it
       takes, with no font-size juggling and no double-scaling. --mark-size-scale is inherited from the
       cards container (DetectionsPage); 1 is the default and a no-op, so the mark is unchanged anywhere the
       property is out of scope (the overlay header, the Cases page). Transform, not width/height, because
       it must scale the glyph and the text pill uniformly too. transform-origin is set per corner below so
       an on-image mark grows INWARD from the corner it is anchored to rather than off the image edge. */
    transform: scale(var(--mark-size-scale, 1));
    transform-origin: top left;
}

/* Grow the on-image mark inward from whichever corner it sits in, so scaling up never pushes it off the
   crop (the image clips overflow). The home top-left needs no override — it matches the base above. */
.detection-grid-card-corner-icon.mark-at-top-right .detection-mark-plate { transform-origin: top right; }
.detection-grid-card-corner-icon.mark-at-bottom-left .detection-mark-plate { transform-origin: bottom left; }
.detection-grid-card-corner-icon.mark-at-bottom-right .detection-mark-plate { transform-origin: bottom right; }

/* The flex child here is MudTooltip's wrapper, not the glyph — DetectionTypeIcon always wraps.
   Inside that wrapper the glyph is INLINE, so two things pull it off centre: Icon.razor writes
   vertical-align:-0.125em into the style attribute (and .icon-layers carries the same), which is
   the right nudge for a glyph sitting next to a label and wrong for one being centred
   geometrically; and the wrapper's line box is sized by the inherited line-height rather than by
   the 1em glyph, so it is taller than its content. The plate then centres the wrapper while the
   ink inside rides low.

   Making the wrapper a flex container fixes both at once: a flex item ignores vertical-align, and
   the wrapper's height collapses to the glyph. Doing it this way rather than overriding
   vertical-align matters — that value is an inline style, so a stylesheet could only beat it with
   !important. Scoped to the plate because the baseline nudge is still correct everywhere the icon
   sits beside text. */
.detection-mark-plate > * {
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Colour turned off in the card settings: the pre-type-colour rendering, a bare mark in the theme's own
   colour. Applied as a class on the container the cards live in rather than per card, so it reaches the
   grid AND the list row without threading a flag through DetectionComponent, the header and into
   DetectionDetailsComponent — which has no card settings of its own.

   The disc is REMOVED, not merely drained: dropping the accent alone would leave a surface-coloured
   circle with a hairline ring, which is a visible piece of furniture rather than the absence of one. So
   the box collapses onto the glyph and the ring and ground both go. */
.no-mark-colors .detection-mark-plate {
    width: auto;
    height: auto;
    border-radius: 0;
    background-color: transparent;
    box-shadow: none;
    color: var(--mud-palette-text-primary);
}

/* Which part of the mark carries the type colour (IconColorClass), when colour is on. The base
   .detection-mark-plate above tints BOTH the disc and the glyph — that stays the fallback for the
   overlay header and Cases page, which have no card settings. On the cards container one of these two
   wins:

   mark-color-bg — the colour is on the DISC. The glyph drops to the theme's text colour so it reads as
   plain ink on a tinted ground.

   mark-color-glyph — the colour is on the GLYPH. The disc drops to a plain surface (its ring kept) and
   the glyph takes the accent, mixed a little toward the text colour so it stays legible in both themes. */
.mark-color-bg .detection-mark-plate {
    color: var(--mud-palette-text-primary);
}

.mark-color-glyph .detection-mark-plate {
    background-color: var(--mud-palette-surface);
    color: color-mix(in srgb, var(--mark-accent, var(--mud-palette-text-primary)) 80%, var(--mud-palette-text-primary));
}

/* Text instead of the glyph (MarkStyle Text): the plate stops being a fixed circle and becomes a pill
   sized to the label, keeping whatever colour treatment the mark-color-* class gave it. The label reads
   the plate's own colour (inherit), so it follows the glyph-vs-background choice for free. */
.mark-as-text .detection-mark-plate {
    width: auto;
    height: auto;
    min-height: 1.25rem;
    border-radius: 999px;
    padding: 2px 8px;
}

.detection-mark-label {
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    color: inherit;
}

/* "Outline the icon for contrast" (card settings, IconOutline). The alternative to the old
   colour-sampling: instead of flipping the glyph white-or-black to suit the crop, keep it in the theme
   colour and give it a halo of the OPPOSITE colour, so whichever brightness the crop happens to be, one
   of the two still reads — a light glyph on a bright image is saved by the dark halo, a dark glyph on a
   dark image by the light one.

   The halo is --mud-palette-surface, which is the theme-aware inverse of the glyph: the plain glyph is
   --mud-palette-text-primary, and surface is (by definition) the thing text-primary is legible against,
   so it flips the right way on the light/dark toggle with nothing in C# having to know the mode — the
   same reason the disc mixes against these vars. Applied to the glyph WRAPPER, not each layer, so a
   multi-layer mark (icon-layers) gets one outline around the composed shape rather than one per layer.

   Four cardinal shadows read as a crisp outline; the fifth soft shadow fills the diagonals so a thin
   stroke does not show gaps at the corners. Independent of the disc: with the disc on it firms up the
   glyph over a faint (low-opacity) disc, with it off it is the only thing keeping the glyph legible.

   Width is driven by --mark-outline-strength (0-100, set on the cards container from the card setting)
   so it can be tuned live — this is a deliberately temporary experiment knob. The px math lives here in
   one calc rather than in C#: strength/100 * 4px, so 25 is 1px (the original fixed look) and 100 is a
   heavy 4px. The fallback 25 keeps the original look anywhere the property is out of scope. */
.mark-outline .detection-mark-plate > * {
    --mark-outline-w: calc(var(--mark-outline-strength, 25) / 100 * 4px);
    filter:
        drop-shadow(var(--mark-outline-w) 0 0 var(--mud-palette-surface))
        drop-shadow(calc(-1 * var(--mark-outline-w)) 0 0 var(--mud-palette-surface))
        drop-shadow(0 var(--mark-outline-w) 0 var(--mud-palette-surface))
        drop-shadow(0 calc(-1 * var(--mark-outline-w)) 0 var(--mud-palette-surface))
        drop-shadow(0 0 var(--mark-outline-w) var(--mud-palette-surface));
}

/* When a detection has no text the mark is overlaid on the top-left of the image instead of taking
   up a second line in the details band. Positioning only — the disc itself is .detection-mark-plate
   above, so the grid overlay and the list row cannot drift apart. The centring is repeated here rather
   than left to the disc so this stays a self-contained overlay box: it positions whatever is inside it,
   with or without the disc class. */
.detection-grid-card-corner-icon {
    position: absolute;
    top: 6px;
    left: 6px;
    z-index: 2;
    pointer-events: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
}

/* The strip is carrying more than the mark — the read text beside it ("detected text beside the icon"),
   the filter dots at the far end, or both. Bounding the right edge is what lets the text truncate
   instead of running off the crop, and it is why the strip has to stop centring its contents: once it
   is wider than the mark, centre would push the mark toward the middle of the image. With the mark
   alone the strip stays content-sized and unchanged. */
.detection-grid-card-corner-icon.spans-width {
    right: 6px;
    justify-content: flex-start;
    min-width: 0;
}

/* Which corner of the image the mark strip sits in (MarkImageCornerClass). The base above is the
   top-left home; these move it, unsetting the base's top/left so the opposite edge wins. Only the home
   pairs with the read text (spans-width), so the others never need the right-edge span. */
.detection-grid-card-corner-icon.mark-at-top-right { top: 6px; left: auto; right: 6px; bottom: auto; }
.detection-grid-card-corner-icon.mark-at-bottom-left { top: auto; left: 6px; right: auto; bottom: 6px; }
.detection-grid-card-corner-icon.mark-at-bottom-right { top: auto; left: auto; right: 6px; bottom: 6px; }

/* Filter dots: one per filter the detection matched, all named in the hover popover. Their own
   absolutely-positioned box in the corner the card setting picked (FilterMarkCorner), above the info
   band. Which corner is a quick experimentation control, so all four are offered — overlap with the
   mark (top-left) or a full-width band (top/bottom) is possible at some corners, which is the whole
   point of being able to try them. */
.detection-grid-card-filter-marks {
    position: absolute;
    z-index: 3;
}

.detection-grid-card-filter-marks.at-top-right { top: 6px; right: 6px; }
.detection-grid-card-filter-marks.at-top-left { top: 6px; left: 6px; }
.detection-grid-card-filter-marks.at-bottom-right { bottom: 6px; right: 6px; }
.detection-grid-card-filter-marks.at-bottom-left { bottom: 6px; left: 6px; }

/* (Removed: a :has()-based rule that hid grid decorations while a sheet was open. It was a workaround for
   the Similar Detections overlay being rendered INSIDE the tile's corner strip — a z-index:2 stacking
   context that trapped the overlay so marks leaked over it. That overlay is now rendered at the card's
   top level (DetectionComponent), where it competes in the root stacking context and covers everything,
   so nothing leaks and no decorations need hiding.) */

/* Filter dots placed IN the info band (FilterMarkPlacement Info*) rather than on the crop. An in-flow
   row that takes its own line among the metadata, aligned to the leading or trailing edge — no absolute
   positioning here, unlike the image placement above. */
.detection-grid-card-info-marks {
    display: flex;
    align-items: center;
    padding: 0 3px;
}

.detection-grid-card-info-marks.align-start { justify-content: flex-start; }
.detection-grid-card-info-marks.align-end { justify-content: flex-end; }

/* The detection-type mark placed IN the band (MarkPlacement Info*) rather than on the crop — an in-flow
   row aligned to the leading or trailing edge, taking its own line among the metadata. */
.detection-grid-card-info-mark {
    display: flex;
    align-items: center;
}

.detection-grid-card-info-mark.align-start { justify-content: flex-start; }
.detection-grid-card-info-mark.align-end { justify-content: flex-end; }

/* Stand-in crop for the card-settings preview, which has no detection and so no photo (see
   DetectionComponent's IsPreview). Literal colours rather than theme tokens on purpose: this is
   standing in for a PHOTOGRAPH, which is not part of the theme, and the settings it exists to
   demonstrate — chip opacity, the mark's disc, the filter dots — all have to be judged against
   something photo-like. Mid-tone and muted so it reads as a crop in either mode without competing with
   the marks drawn on top of it. */
.detection-grid-card-image-stand-in {
    width: 100%;
    height: 100%;
    background: linear-gradient(135deg, #6b7a5a 0%, #3a4a5a 60%, #8a7a6a 100%);
}

/* Box for the FallbackImage control. There is deliberately NO placeholder drawn behind the image: an
   always-present underlay cost an extra <img> per instance (and made the grid's corner-ink sampler run
   twice per card) and bled through the letterbox bars of a Contain-fitted image. Instead a missing image
   renders the placeholder directly, and one that 404s is swapped to the logo by site.js. */
.stx-fallback-image {
    position: relative;
    overflow: hidden;
}

.stx-fallback-image-fill {
    width: 100%;
    height: 100%;
}

.stx-fallback-image-img {
    width: 100%;
    height: 100%;
    display: block;
}

/* The "Not found" placeholder, revealed BEHIND-then-in-place-of an image that failed to load (a 404).
   Hidden until then via an inline display:none so it never bleeds through a loaded image's letterbox;
   stxShowFallback (site.js) flips it on and hides the failed image. Absolutely fills the image box. */
.stx-image-fallback-underlay {
    position: absolute;
    inset: 0;
}

/* An image the browser could not load, marked js-image-fallback: hidden so the browser's own broken-image
   glyph never shows. Used on the placeholder's own logo <img> — if the logo asset is unavailable the "Not
   found" caption beside it stands alone rather than a broken icon. site.js adds this class on error. */
.js-image-fallback.stx-image-failed {
    display: none;
}

.detection-filter-marks {
    display: flex;
    align-items: center;
    gap: 3px;
    cursor: default;
}

/* Small on purpose: the dot is a cue that the popover has the detail, not a legend. The ring is
   load-bearing rather than decorative — white and yellow filters are otherwise invisible on a light
   surface, and an uncoloured filter (FilterColor.NoColor, transparent background) is drawn by the ring
   alone, which is what lets a match the card tint cannot show still be visible and named. */
.detection-filter-mark {
    width: 10px;
    height: 10px;
    flex: 0 0 auto;
    border-radius: 50%;
    box-shadow: inset 0 0 0 1px rgb(0 0 0 / 45%), 0 0 0 1px rgb(255 255 255 / 55%);
}

.detection-filter-mark-overflow {
    font-size: 0.6rem;
    line-height: 1;
    padding: 1px 3px;
    border-radius: 3px;
    color: var(--mud-palette-white);
    background-color: rgb(0 0 0 / 55%);
}

/* The text chip beside the mark. Same background as an on-image metadata value — one --mark-chip, so the
   opacity slider moves both together and the two never disagree about how much of the photo shows
   through. White ink because it is over an arbitrary crop, matching the on-image band.

   min-width: 0 on every descendant for the same reason the band needs it: MudStack renders utility
   classes rather than a hook of its own, so the flex chain cannot shrink below the text's nowrap
   min-content without it, and the chip would push past the card edge instead of truncating. */
.detection-grid-card-corner-text {
    min-width: 0;
    padding: 1px 5px;
    border-radius: 3px;
    background-color: var(--mark-chip, rgb(0 0 0 / 70%));
    color: var(--mud-palette-white);
}

.detection-grid-card-corner-text * {
    min-width: 0;
}

.detection-grid-card-corner-text .mud-typography {
    font-size: 0.8rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Customized card display (user preference): the info band placed ON the image instead of
   below it. The band keeps .detection-grid-card-details so all the truncation rules above
   still apply; the host class makes the card the positioning context. White text over a
   translucent scrim (the preference model supplies the rgba background). */
.detection-grid-card-overlay-host {
    position: relative;
}

.detection-grid-card-info-overlay {
    position: absolute;
    left: 0;
    right: 0;
    z-index: 2;
    color: var(--mud-palette-white);
    border-radius: 0;
}

.detection-grid-card-info-overlay.overlay-top {
    top: 0;
}

/* The mark is an absolutely-positioned overlay in the image's top-left (6px inset, 2rem tall), so a
   top-placed band starting at y=0 covers it. Start the band below the mark instead. The class is only
   applied when a mark actually renders (DetectionComponent), so a card without one keeps the full
   image rather than a mark-sized gap. 4px of breathing room under the mark. */
.detection-grid-card-info-overlay.overlay-top.clears-mark {
    top: calc(6px + 2rem + 4px);
}

.detection-grid-card-info-overlay.overlay-bottom {
    bottom: 0;
}

/* ON-IMAGE placement only: one value per line, each tinted just behind its own text.

   Two things change from the below-the-image band. The rows run as a COLUMN, so every value gets its
   own line instead of sharing a flex row — on a crop, values sharing a line read as one run of text
   and the eye cannot tell where one ends. And each value carries its own background (--mark-chip, the
   detection type's accent at the configured opacity, set by DetectionComponent), so the tint hugs the
   text and the photo shows through the gaps. The band itself is transparent here — a full-width scrim
   covered the part of the picture the operator is looking at, which is the reason to put the info on
   the image at all.

   align-items/align-self flex-start is what makes a chip only as wide as its text: in a column flex
   container the default stretch would run every chip to the full card width and put the gaps back. */
.detection-grid-card-info-overlay .detection-grid-card-header-line {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

.detection-grid-card-info-overlay .detection-grid-card-timestamp,
.detection-grid-card-info-overlay .detection-grid-card-device-name,
.detection-grid-card-info-overlay .detection-grid-card-field {
    flex: 0 0 auto;
    align-self: flex-start;
    max-width: 100%;
    /* The device name is right-aligned in the below-image band, where it shares a row with the
       timestamp. On its own line that would strand it against the far edge. */
    text-align: left;
    padding: 1px 5px;
    border-radius: 3px;
    background-color: var(--mark-chip, rgb(0 0 0 / 70%));
    /* These values carry opacity 0.75 for a solid band. Over a photo that dims the chip along with the
       text and costs the contrast the chip exists to provide — the translucency is the chip's job now. */
    opacity: 1;
}

/* A user-selected value field on the grid card (confidence, colors, vehicle, address, …).
   Compact like the timestamp, but NOT dimmed like it: the time and channel are context, these are
   the thing the card is about. Age and gender at the timestamp's 0.75 were hard to pick out of a
   row that is otherwise chrome. Still allowed to shrink and truncate, though the metadata now has
   a line of its own (DetectionCardSettings.GroupBandRows) so there is room before it comes to that. */
.detection-grid-card-field {
    flex: 0 1 auto;
    font-size: 0.7rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}


#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }


.scroll-locked {
    padding-right: 9px;
}

/* .table-row-header / .table-row-selected moved to brand.css (colors ride the palette now). */

.drawer-background {
    background-color: var(--mud-palette-background);
}

.iframe {
    height: 400px;
    width: 100%;
    margin: 0 !important;
}

.setup-floating-buttons {
    position: fixed;
    bottom: 10px;
    right: 10px;
    z-index: 1100;
}

.dialog-popover {
    z-index: 1502;
}

.one-column-component {
    height: calc(100vh - 200px);
    width: 800px;
    overflow-x: hidden;
    overflow-y: auto;
}

.two-column-component {
    height: calc(100vh - 200px);
    width: 1200px;
    overflow-x: hidden;
    overflow-y: auto;
}

.three-column-component {
    height: calc(100vh - 200px);
    width: 1600px;
    overflow-x: hidden;
    overflow-y: auto;
}

.overlay-w1 {
    width: 800px;
    overflow-x: hidden;
}

.overlay-w2 {
    width: 1000px;
    overflow-x: hidden;
}

.overlay-w3 {
    width: 1200px;
    overflow-x: hidden;
}

.overlay-w4 {
    width: 1400px;
    overflow-x: hidden;
}

.overlay-w5 {
    width: 1600px;
    overflow-x: hidden;
}

.overlay-h1 {
    height: calc(100vh - 500px);
    overflow-y: auto;
}

.overlay-h2 {
    height: calc(100vh - 400px);
    overflow-y: auto;
}

.overlay-h3 {
    height: calc(100vh - 300px);
    overflow-y: auto;
}

.overlay-h4 {
    height: calc(100vh - 200px);
    overflow-y: auto;
}

.overlay-h5 {
    height: calc(100vh - 100px);
    overflow-y: auto;
}

/* The filter editor hosts raw MudTabs inside the overlay pane. The active panel is
   display:contents, so its MudGrid's negative spacing margin overhangs the tab bar
   and hit-tests in front of the lower half of every tab — lift the bar above the
   panels so clicks land on the tabs across their full height. */
.filter-editor-tabs .mud-tabs-tabbar {
    position: relative;
    z-index: 1;
}

.detections-page {
    height: calc(100vh - var(--mud-appbar-height));
    overflow: hidden;
    position: relative;
    padding-right: 12px;
    padding-left: 12px;
    display: flex;
    flex-direction: column;
}

.scroll-position-wrapper
{
    position: relative;
    flex: 1;
    min-height: 0;
}

.scrollable-container
{
    /* Margin/padding values below set the scrollbar flush with the right edge of the container */
    height: 100%;
    margin-right: -12px;
    padding-right: 12px;
    overflow-y: auto;
}

.scroll-position-indicator
{
    position: absolute;
    right: 22px;
    top: 0;
    background: var(--mud-palette-dark);
    color: var(--mud-palette-dark-text);
    padding: 4px 14px;
    border-radius: 4px;
    font-size: 0.8rem;
    font-weight: 500;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
    z-index: 10;
    white-space: nowrap;
}

.scroll-position-indicator.visible
{
    opacity: 1;
}

.page {
    height: calc(100vh - var(--mud-appbar-height));
    overflow-x: hidden;
    position: relative;
    padding: 12px;
}

.navmenu {
    overflow-y: scroll;
    scrollbar-width: none;
    overflow-x: hidden;
}

.mud-drawer-content {
    overflow-x: hidden !important;
}

.camera-list-item:hover {
    background-color: var(--mud-palette-action-default-hover);
}

.view-item-actions {
    opacity: 0;
    transition: opacity 0.15s;
}

.camera-list-item:hover .view-item-actions {
    opacity: 1;
}

.navmenu::-webkit-scrollbar {
    display: none;
}

.page-content {
    overflow-x: hidden;
    overflow-y: auto;
}

.index-page {
    height: calc(100vh - 112px);
    overflow-x: hidden;
    position: relative;
    padding: 12px;
}

.help-page {
    height: calc(100vh - 24px);
    overflow-x: hidden;
    position: relative;
    padding: 12px;
}

.help-page-content {
    height: calc(100vh - 119px);
    overflow-y: auto;
}

/* In-app help (HelpModule). The full /Help page uses the shared StandardDrawerLayout; these
   keep the content pane scrolling internally (not the whole page) and tidy the typography. */
.help-browser {
    flex: 1 1 auto;
    min-height: 0; /* let the flex children below own the scroll (dialog use) */
}

.help-browser__nav {
    overflow-y: auto;
    min-height: 0;
}

.help-browser__content {
    overflow-y: auto;
    min-height: 0;
}

/* Child sections (e.g. an edit dialog's tabs) sit indented under their parent topic. */
.help-nav-child {
    padding-left: 2.25rem !important;
    font-size: 0.92em;
    opacity: 0.92;
}

/* Centered, comfortable reading column with margins. */
.help-article {
    max-width: 980px;
    margin: 0 auto;
}

/* Readable markdown typography. */
.markdown-body {
    line-height: 1.65;
}

.markdown-body h2 {
    margin: 1.6em 0 0.5em;
    font-size: 1.25rem;
    font-weight: 600;
}

.markdown-body h3 {
    margin: 1.2em 0 0.4em;
    font-size: 1.05rem;
    font-weight: 600;
}

.markdown-body p,
.markdown-body li {
    margin: 0.4em 0;
}

.markdown-body ul {
    padding-left: 1.4em;
}

.markdown-body blockquote {
    margin: 1.2em 0;
    padding: 0.6em 1em;
    border-left: 3px solid var(--mud-palette-primary);
    background: var(--mud-palette-action-default-hover);
    border-radius: 4px;
}

.markdown-body blockquote p {
    margin: 0;
}

/* Screenshots: modest size, centered, click to zoom. */
.markdown-body img {
    display: block;
    max-width: 100%;
    max-height: 420px;
    width: auto;
    margin: 18px auto;
    border-radius: 6px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
    cursor: zoom-in;
}

.help-lightbox {
    position: fixed;
    inset: 0;
    z-index: 4000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2vmin;
    background: var(--mud-palette-overlay-dark);
    cursor: zoom-out;
}

.help-lightbox img {
    max-width: 96vw;
    max-height: 96vh;
    border-radius: 4px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6);
}

.table {
    margin-top: 12px;
    margin-left: 12px;
    margin-right: 12px;
    margin-bottom: 8px;
}

.drawer {
    background-color: transparent;
    max-width: 100vw;
    padding-bottom: 12px;
    padding-left: 12px;
    padding-right: 4px;
    max-height: calc(100vh - var(--mud-appbar-height) - 12px);
}

.search-drawer {
    height: calc(100vh - 100px);
    overflow-x: hidden;
    overflow-y: auto;
    margin-top: -12px;
    margin-bottom: -12px;
    margin-right: -4px;
    margin-left: -12px;
    padding-top: 12px;
    padding-bottom: 8px;
    padding-left: 12px;
    padding-right: 10px;
}

.search-drawer-buttons {
    margin-top: 19px;
    margin-right: 6px;
    margin-left: -2px;
}

/* Asset list page content area — CSS Grid layout that auto-sizes the active
   list/grid view to fill the available space. Row 1 (auto) is the expansion
   panel form; row 2 (1fr) is the active grid (face / text / read-only fallback)
   which gets a definite computed height from the 1fr track. The grid component
   uses Height="100%" against that track. Replaces the previous magic-number
   calc(100vh - Xpx) approach so layout changes above (or panel toggles) just
   work without retuning anything. */
.asset-list-content-grid {
    display: grid;
    grid-template-rows: auto 1fr;
    height: 100%;
    min-height: 0;
}

/* Force the active grid view to fill the 1fr row. Targets MudDataGrid for the
   editable views and the asset-grid-fill MudPaper for the read-only fallback.
   The "Save first" notice MudPaper is intentionally NOT included — it stays
   at natural size so we don't inflate one line of text into a giant box. */
.asset-list-content-grid > .mud-data-grid,
.asset-list-content-grid > .asset-grid-fill {
    height: 100%;
    min-height: 0;
}

/* MudDataGrid's outer wrapper is block-layout by default, so the toolbar +
   table-container (with max-height: 100%) + pager can stack PAST the wrapper's
   definite height — pushing the page past the drawer. Force flex column so the
   table-container fills only the REMAINING space after toolbar + pager. Scoped
   to .asset-list-content-grid so other MudDataGrids in the app aren't affected. */
.asset-list-content-grid > .mud-data-grid {
    display: flex;
    flex-direction: column;
}

.asset-list-content-grid > .mud-data-grid > .mud-table-container {
    flex: 1 1 0;
    min-height: 0;
}

/* Search drawer's vertical layout — full-height flex column so the Search/Reset
   action buttons stay pinned to the bottom regardless of expansion-panel scroll. */
.search-vertical-layout {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}

.search-vertical-layout > .search-vertical-scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    min-height: 0;
}

.search-vertical-layout > .search-vertical-actions {
    flex: 0 0 auto;
    padding-bottom: 8px;
    margin-top: 8px;
}

/* Asset list page content area — CSS Grid layout that auto-sizes the active
   list/grid view to fill the available space. Row 1 (auto) is the expansion
   panel form; row 2 (1fr) is the active grid (face / text / read-only fallback)
   which gets a definite computed height from the 1fr track. The grid component
   uses Height="100%" against that track. Replaces the previous magic-number
   calc(100vh - Xpx) approach so layout changes above (or panel toggles) just
   work without retuning anything. */
.asset-list-content-grid {
    display: grid;
    grid-template-rows: auto 1fr;
    height: 100%;
    min-height: 0;
}

/* Force the active grid view to fill the 1fr row. Targets MudDataGrid for the
   editable views and the asset-grid-fill MudPaper for the read-only fallback.
   The "Save first" notice MudPaper is intentionally NOT included — it stays
   at natural size so we don't inflate one line of text into a giant box. */
.asset-list-content-grid > .mud-data-grid,
.asset-list-content-grid > .asset-grid-fill {
    height: 100%;
    min-height: 0;
}

/* MudDataGrid's outer wrapper is block-layout by default, so the toolbar +
   table-container (with max-height: 100%) + pager can stack PAST the wrapper's
   definite height — pushing the page past the drawer. Force flex column so the
   table-container fills only the REMAINING space after toolbar + pager. Scoped
   to .asset-list-content-grid so other MudDataGrids in the app aren't affected. */
.asset-list-content-grid > .mud-data-grid {
    display: flex;
    flex-direction: column;
}

.asset-list-content-grid > .mud-data-grid > .mud-table-container {
    flex: 1 1 0;
    min-height: 0;
}

/* Search drawer's vertical layout — full-height flex column so the Search/Reset
   action buttons stay pinned to the bottom regardless of expansion-panel scroll. */
.search-vertical-layout {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}

.search-vertical-layout > .search-vertical-scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    min-height: 0;
}

.search-vertical-layout > .search-vertical-actions {
    flex: 0 0 auto;
    padding-bottom: 8px;
    margin-top: 8px;
}

/* Detection-type tab selector inside the search drawer: hide MudTabs' overflow scroll
   arrows so all icons fit in the drawer width without the chevrons. */
.detection-type-tabs .mud-tabs-toolbar-scroll-button,
.detection-type-tabs .mud-tab-scroll-button,
.detection-type-tabs .mud-tabs-scroll-button {
    display: none !important;
}

/* Reclaim the default expansion-panel inner padding inside the search drawer so the
   detection-type tabs have more room. */
.search-drawer-panels .mud-expand-panel-content {
    padding: 4px !important;
}

.watch-list-red {
    background-color: #d98880;
}

.watch-list-orange {
    background-color: #f0b27a;
}

.watch-list-yellow {
    background-color: #f7dc6f;
}

.watch-list-light-green {
    background-color: #82e0aa;
}

.watch-list-green {
    background-color: #7dcea0;
}

.watch-list-blue-green {
    background-color: #73c6b6;
}

.watch-list-blue {
    background-color: #85c1e9;
}

.watch-list-light-blue {
    background-color: #d4e6f1;
}

.watch-list-lavender {
    background-color: #e8daef;
}

.watch-list-purple {
    background-color: #bb8fce;
}

.watch-list-pink {
    background-color: #fadbd8;
}

.watch-list-white {
    background-color: #FFFFFF;
}

.dialog-background {
    backdrop-filter: blur(10px);
}

#components-reconnect-modal {
    color: black !important;
}

.truncate-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 240px;
}

[data-md-render] ul {
    list-style-position: inside;
}

[data-md-render] p {
    margin-bottom: .25em;
}

[data-md-render] h3 {
    font-size: var(--mud-typography-h3-size);
    font-family: var(--mud-typography-h3-family);
    font-weight: var(--mud-typography-h3-weight);
    line-height: var(--mud-typography-h3-lineheight);
    letter-spacing: var(--mud-typography-h3-letterspacing);
    text-transform: var(--mud-typography-h3-text-transform);
    margin-top: 1rem;
    margin-bottom: .8rem;
}

[data-md-render] h4 {
    font-size: var(--mud-typography-h4-size);
    font-family: var(--mud-typography-h4-family);
    font-weight: var(--mud-typography-h4-weight);
    line-height: var(--mud-typography-h4-lineheight);
    letter-spacing: var(--mud-typography-h4-letterspacing);
    text-transform: var(--mud-typography-h4-text-transform);
    margin-top: 1rem;
    margin-bottom: .75rem;
}

[data-md-render] h5 {
    font-size: var(--mud-typography-h5-size);
    font-family: var(--mud-typography-h5-family);
    font-weight: var(--mud-typography-h5-weight);
    line-height: var(--mud-typography-h5-lineheight);
    letter-spacing: var(--mud-typography-h5-letterspacing);
    text-transform: var(--mud-typography-h5-text-transform);
    margin-top: 1rem;
    margin-bottom: .5rem;
}

[data-md-render] h6 {
    font-size: var(--mud-typography-h6-size);
    font-family: var(--mud-typography-h6-family);
    font-weight: var(--mud-typography-h6-weight);
    line-height: var(--mud-typography-h6-lineheight);
    letter-spacing: var(--mud-typography-h6-letterspacing);
    text-transform: var(--mud-typography-h6-text-transform);
    margin-top: 1rem;
    margin-bottom: .25rem;
}

.audit-trail-grid {
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.audit-trail-grid .mud-table-container {
    flex: 1 1 0;
    min-height: 0;
}

.audit-trail-grid td:first-child button:disabled {
    visibility: hidden;
}

.smart-search-container {
    min-width: 500px;
    max-width: 800px;
}

.page-image-drop-banner {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 160px;
    z-index: 1500;
    background: rgba(var(--mud-palette-primary-rgb), 0.95);
    color: var(--mud-palette-primary-contrasttext);
    align-items: center;
    justify-content: center;
    gap: 12px;
    border-bottom: 3px dashed var(--mud-palette-primary-contrasttext);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
    animation: drop-banner-slide 0.2s ease-out;
}

.page-image-drop-banner.drag-active {
    display: flex;
}

@keyframes drop-banner-slide {
    from { transform: translateY(-100%); }
    to { transform: translateY(0); }
}

.detection-type-card {
    border: 1px solid var(--mud-palette-lines-default) !important;
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, background-color 0.15s ease !important;
}

.detection-type-card:hover {
    border-color: rgba(var(--mud-palette-primary-rgb), 0.6) !important;
    background-color: rgba(var(--mud-palette-primary-rgb), 0.1) !important;
    transform: translateY(-2px);
    box-shadow: 0 6px 16px 0 rgba(var(--mud-palette-primary-rgb), 0.25) !important;
}

.detection-type-card-selected {
    border: 1px solid rgba(var(--mud-palette-primary-rgb), 0.6) !important;
    background-color: rgba(var(--mud-palette-primary-rgb), 0.1) !important;
}

.detection-type-card-selected:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px 0 rgba(var(--mud-palette-primary-rgb), 0.4) !important;
}

.object-grid {
    display: grid;
    gap: 0.5rem;
    width: 100%;
}

.object-grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.object-grid-cols-5 { grid-template-columns: repeat(5, 1fr); }
.object-grid-cols-6 { grid-template-columns: repeat(6, 1fr); }
.object-grid-cols-7 { grid-template-columns: repeat(7, 1fr); }

.object-grid-tile {
    height: 80px !important;
    min-height: 0 !important;
}

.color-swatch-grid {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 0.25rem;
    width: 100%;
}

.color-swatch {
    aspect-ratio: 1 / 1 !important;
    width: 100% !important;
    height: auto !important;
    min-width: 0 !important;
    min-height: 0 !important;
    padding: 0 !important;
    border: none !important;
}

.wizard-step-body {
    width: 75%;
    margin: 0 auto;
}

/* The equal-width action buttons. A class rather than an inline width, so the measure can be
   dropped below md — an inline style would need !important to override. */
.wizard-action-button {
    width: 110px;
}

/* Below md, 75% is a desktop measure leaking onto a phone: it spent a quarter of the screen on
   dead margin either side and squeezed every criteria card into the remaining 270px, which is what
   clipped the LPR match-mode toggles. The action row takes the same class so the buttons stay
   aligned with the card edges above md and go full width with it below.

   The buttons lose their fixed width at the same breakpoint. Three at 110px plus the row's own
   px-4 comes to ~362px, which overflowed a 360px phone even at full width — they size to their
   labels here and the row fits. */
/* The compact step indicator is the phone's replacement for the nav strip, so the two are exact
   opposites: strip on and indicator off above md, reversed below. */
.wizard-mobile-step {
    display: none;
}

@media (max-width: 959px) {
    .wizard-step-body {
        width: 100%;
    }

    .wizard-action-button {
        width: auto;
    }

    /* MudStepper has no breakpoint-aware mode and ScrollableNavigation defaults to true, so on a
       phone this strip is a horizontally scrolling row that costs a whole band of the screen and
       still shows two and a half of six steps. Hidden via the stepper's own NavClass hook.

       The .mud-stepper.mud-stepper__horizontal prefix is not decoration — it is the specificity.
       MudBlazor ships `.mud-stepper.mud-stepper__horizontal>.mud-stepper-nav { display: flex }` at
       (0,3,0); a bare `.wizard-nav` is (0,1,0) and loses, which is why the strip was still on screen
       under the compact indicator that replaced it. Matching (0,3,0) and loading after MudBlazor
       wins the tie. */
    .mud-stepper.mud-stepper__horizontal > .wizard-nav {
        display: none;
    }

    /* MudStepper pads its content 24px each side (.mud-stepper .mud-stepper-content). That is another
       desktop measure: on a phone it spends 48px of the card's width on gutter, on top of whatever
       the page around it already reserves. 8px here still separates the card from the screen edge.
       (0,2,0) matches MudBlazor's own selector and this file loads later, so it wins the tie. */
    .mud-stepper .mud-stepper-content {
        padding: 0 8px;
    }

    .wizard-mobile-step {
        display: block;
    }
}

/* Overlay title bar (TitleBar): the action buttons used to be stretched to the bar's full height
   and squared off here, which put them hard against the sheet's rounded top corner — the corner
   clipped the last one. They now sit at their own size, centred and inset, which is layout the
   markup can express on its own, so nothing about the strip needs a rule in this file. The
   .overlay-title-bar class stays on the bar: E2E scopes its title-bar locators to it. */

/* Tooltips are positioned in VIEWPORT coordinates, not document coordinates.

   MudBlazor places a popover absolutely, and a body-level one (Inline="false") in the document's
   coordinate space — so a tooltip whose box reaches past the right edge of the window grows
   documentElement.scrollWidth. Measured on the Detections toolbar in a 900px viewport: a
   right-anchored icon button's tooltip landed 39px past the edge, scrollWidth went 900 -> 929, and
   the horizontal scrollbar that appeared took clientWidth down to 890. Those 10px are the bug — that
   toolbar is anchored `right: 16px`, so the button slid out from under the pointer, the tooltip
   closed, the scrollbar went away, the button slid back, and the tooltip reopened: the page flicker.

   A fixed box contributes nothing to the document's scrollable overflow, so a viewport-positioned
   tooltip cannot start that loop at any window size. This selector is the zero-size marker MudBlazor
   leaves at the anchor site as the popover's placement handle; its own JS reads this element's
   computed position and switches the popover to viewport coordinates when it finds `fixed`, which is
   the same branch MudPopover's `Fixed` parameter takes. MudTooltip does not surface that parameter,
   which is why this is CSS: the shared Button control asks for it directly with
   Class="mud-popover-fixed", and this rule covers the raw <MudTooltip> call sites — around 74 of
   them — that have not moved onto that control yet. The handle carries no content and is already out
   of flow, so no button's layout changes. */
.mud-tooltip-root > [id^="popover-"] {
    position: fixed;
}

/* A tooltip is placed centred on its button and is then squeezed by whichever viewport edge it
   reaches, so its text has to be able to wrap into the room that is left. A label with nowhere to
   break — a reference ID, an underscored device name, an email address — instead keeps its full
   width and hangs past the edge, where the rule above leaves it clipped rather than readable.
   `anywhere` and not `break-word` because only `anywhere` lowers the box's min-content width, and
   that is the measurement that decides whether it fits at all. */
.mud-popover.mud-tooltip {
    overflow-wrap: anywhere;
}

/* The dashboard's Latest Detections widget is a fixed 410px card and gives its table 350px, which has
   to hold a 35px header plus ten rows — about 31px a row. Two things put it over that budget and made
   the table scroll (measured: 412px of content in 350px):

   The shared mark is a fixed 2rem circle, so ten rows have a 33px floor = 365px on their own, before
   any text. And the widget's Detection column is 136px wide with 40px of cell padding, leaving 60px
   for the label — narrower than "Red Car" (61px), which therefore wrapped onto a second line and took
   those rows to 49px.

   So the mark is 1.75rem here and the label is kept to one line. 28px still holds the widest glyph
   (Military/Motorcycle/Bicycle are 25px at Size.Small — measured, no overhang), and trimming that
   column's padding to px-1 in the markup gives the label 96px, enough that the labels which fit on one
   line today still do rather than being ellipsised. Ten rows now measure 324px in the 350px, so the
   table has ~26px spare instead of 62px too much. The mark keeps its full 2rem everywhere it is not
   inside a height-boxed widget — the detections list and grid, where it is the primary read.

   Scoped by the class on this one MudTable rather than by element position, so reordering the columns
   cannot silently move the rule onto a different one. */
.latest-detections-table .detection-mark-plate {
    width: 1.75rem;
    height: 1.75rem;
}

.latest-detections-table .mud-typography {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
