/* VisionX Cam surveillance console.
   A dark surface is the default on purpose: an operator watching a wall of cameras for a long
   stretch should have the picture be the brightest thing on screen, not the chrome around it. */

:root {
    color-scheme: dark;
    --vx-bg: #0d1117;
    --vx-surface: #161b22;
    --vx-surface-raised: #1c2430;
    --vx-border: #2d3644;
    --vx-text: #e6edf3;
    --vx-text-muted: #8b98a8;
    --vx-accent: #2f81f7;
    --vx-accent-hover: #4b96ff;
    --vx-danger: #f85149;
    --vx-ok: #3fb950;
    --vx-warn: #d29922;
    --vx-radius: 8px;
    --vx-radius-lg: 12px;
    --vx-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);

    /* Everything a finger has to hit is at least this tall. 44px is the smallest target that a
       thumb reliably lands on, and this app is now read at arm's length on a phone. */
    --vx-tap: 2.75rem;

    /* Gutters shrink on a phone, where 20px of chrome down each side is a fifth of the screen. */
    --vx-gutter: 1.25rem;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: var(--vx-bg);
    color: var(--vx-text);
    font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
    font-size: 15px;
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

a {
    color: var(--vx-accent);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

/* One focus ring for the whole console. Without it a keyboard — or a phone's switch access — walks
   through the app invisibly, because every control here paints its own background. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--vx-accent);
    outline-offset: 2px;
}

/* A form control under 16px makes mobile Safari zoom the page on focus and never zoom back out. */
input,
select,
textarea {
    font-family: inherit;
    font-size: 16px;
}

.vx-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.vx-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 1.25rem;
    padding: 0.6rem var(--vx-gutter);
    background: var(--vx-surface);
    border-bottom: 1px solid var(--vx-border);
    position: sticky;
    top: 0;
    z-index: 10;

    /* The nav wraps onto a second line rather than pushing the document sideways. A console that
       scrolls horizontally on a phone is one where every page is slightly off-centre and no amount
       of care in the page itself fixes it. */
    flex-wrap: wrap;
}

.vx-brand {
    font-weight: 600;
    font-size: 1.05rem;
    letter-spacing: 0.02em;
    color: var(--vx-text);
    white-space: nowrap;
}

.vx-brand span {
    color: var(--vx-accent);
}

.vx-nav {
    display: flex;
    gap: 0.35rem;
    margin-left: auto;
    /* A phone is narrower than four labels. The rail scrolls rather than clipping,
       because a destination that cannot be reached might as well not exist —
       Server was unreachable on the first real handset this app ever ran on. */
    overflow-x: auto;
    max-width: 100%;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}

/* The rail still scrolls; it just does not put a scrollbar through the middle of the header to say
   so. On the width where scrolling is actually needed the input is a thumb, not a bar. */
.vx-nav::-webkit-scrollbar {
    height: 0;
}

.vx-nav a {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 2.25rem;
    padding: 0.35rem 0.8rem;
    border-radius: var(--vx-radius);
    color: var(--vx-text-muted);
    white-space: nowrap;
}

.vx-nav a:hover,
.vx-nav a.active {
    background: var(--vx-surface-raised);
    color: var(--vx-text);
    text-decoration: none;
}

.vx-nav a.active {
    font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--vx-accent);
}

.vx-main {
    flex: 1;
    padding: var(--vx-gutter);
}

/* ---- The phone ---------------------------------------------------------------------------------
   Below this width the brand and four destinations cannot share a line without one of them being
   squeezed, so the header becomes two honest rows: who this is, then where you can go. The nav row
   is the full width of the screen and the destinations divide it evenly, which is also what makes
   every one of them a 44px target. */

@media (max-width: 640px) {
    :root {
        --vx-gutter: 0.9rem;
    }

    .vx-header {
        gap: 0.5rem 0;
        padding: 0.55rem var(--vx-gutter) 0.5rem;
    }

    .vx-brand {
        flex: 1 0 100%;
        font-size: 1rem;
    }

    .vx-nav {
        flex: 1 0 100%;
        margin-left: 0;
        gap: 0.25rem;
    }

    /* Grow to share the row, but never shrink below the label: a rail that cannot fit still scrolls
       rather than truncating a destination out of reach. */
    .vx-nav a {
        flex: 1 0 auto;
        min-height: var(--vx-tap);
        padding: 0.35rem 0.6rem;
    }
}

/* Camera wall ------------------------------------------------------------ */

.vx-wall {
    display: grid;
    gap: 0.9rem;
    /* min() rather than a bare 340px: on a 360px handset a fixed minimum makes the track wider than
       the screen and the whole wall scrolls sideways. */
    grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
}

.vx-wall[data-columns="1"] {
    grid-template-columns: 1fr;
}

.vx-tile {
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius-lg);
    box-shadow: var(--vx-shadow);
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* The picture is the top of the card and runs edge to edge; everything that describes it sits
   underneath on one line. Nothing is written over the video, because a caption laid across a
   camera's own picture is unreadable the moment the scene behind it is bright. */

.vx-tile-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: var(--vx-tap);
    padding: 0.25rem 0.35rem 0.25rem 0.7rem;
    border-top: 1px solid var(--vx-border);
}

.vx-tile-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.vx-tile-actions {
    margin-left: auto;
    display: flex;
    flex: none;
    gap: 0.25rem;
}

.vx-tile-actions .vx-button {
    min-width: var(--vx-tap);
    padding: 0.3rem;
    font-size: 1rem;
    line-height: 1;
}

/* The state of the stream, as one chip: the lamp on its own while it is coming up or failing, the
   lamp and the word once there is actually a live picture to describe. */

.vx-tile-state {
    display: inline-flex;
    align-items: center;
    flex: none;
    gap: 0.35rem;
    border-radius: 999px;
}

/* Only the live state earns a chip around it. While the stream is coming up or has failed the lamp
   speaks for itself, and a grey pill around a grey dot is just another thing on the line. */
.vx-tile-state[data-state="playing"] {
    padding: 0.12rem 0.5rem 0.12rem 0.45rem;
    background: rgba(63, 185, 80, 0.12);
    box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.35);
}

.vx-tile-state-text {
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--vx-ok);
}

.vx-stage {
    position: relative;
    background: #000;
    aspect-ratio: 16 / 9;
}

.vx-stage video {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: contain;
    background: #000;
}

.vx-stage-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 1rem;
    color: var(--vx-text-muted);
    pointer-events: none;
}

.vx-stage-overlay.vx-error {
    color: var(--vx-danger);
}

/* Status pip ------------------------------------------------------------- */

.vx-pip {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--vx-text-muted);
    flex: none;
}

.vx-pip[data-state="playing"] {
    background: var(--vx-ok);
}

.vx-pip[data-state="connecting"],
.vx-pip[data-state="buffering"] {
    background: var(--vx-warn);
}

.vx-pip[data-state="error"],
.vx-pip[data-state="closed"] {
    background: var(--vx-danger);
}

/* PTZ pad ---------------------------------------------------------------- */

.vx-ptz {
    display: grid;
    grid-template-columns: repeat(3, var(--vx-tap));
    grid-template-rows: repeat(3, var(--vx-tap));
    gap: 0.25rem;
    padding: 0.6rem 0.75rem;
    border-top: 1px solid var(--vx-border);
    justify-content: center;
}

.vx-ptz-zoom {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    justify-content: center;
    padding: 0.6rem 0.5rem 0.6rem 0;
}

.vx-ptz-row {
    display: flex;
    align-items: center;
    justify-content: center;
    border-top: 1px solid var(--vx-border);
}

.vx-ptz-row .vx-ptz {
    border-top: none;
}

.vx-ptz button,
.vx-ptz-zoom button {
    background: var(--vx-surface-raised);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    cursor: pointer;
    font-size: 1rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    touch-action: manipulation;
    user-select: none;
}

.vx-ptz-zoom button {
    width: var(--vx-tap);
    height: var(--vx-tap);
}

.vx-ptz button:hover,
.vx-ptz-zoom button:hover {
    border-color: var(--vx-accent);
}

.vx-ptz button:active,
.vx-ptz-zoom button:active {
    background: var(--vx-accent);
    border-color: var(--vx-accent);
}

.vx-ptz .vx-ptz-centre {
    background: transparent;
    border: none;
    cursor: default;
    color: var(--vx-text-muted);
    font-size: 0.7rem;
}

/* Forms and buttons ------------------------------------------------------ */

/* Each section on the add-camera page is one of these: a card with a ruled heading, so the three
   ways in — a URL you type, a network you scan, a device that dials you — read as three separate
   choices rather than one long form. */

.vx-panel {
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius-lg);
    box-shadow: var(--vx-shadow);
    padding: 1.1rem;
    max-width: 46rem;
}

.vx-panel + .vx-panel {
    margin-top: 1rem;
}

.vx-panel h2 {
    margin: 0 0 0.7rem;
    padding-bottom: 0.55rem;
    border-bottom: 1px solid var(--vx-border);
    font-size: 1.05rem;
}

.vx-panel p.vx-hint {
    margin: 0 0 1rem;
    color: var(--vx-text-muted);
    font-size: 0.9rem;
}

.vx-panel > .vx-button {
    margin-top: 0.25rem;
}

.vx-field {
    margin-bottom: 0.85rem;
}

.vx-field label {
    display: block;
    margin-bottom: 0.3rem;
    font-size: 0.85rem;
    color: var(--vx-text-muted);
}

.vx-field input,
.vx-field select {
    width: 100%;
    min-height: var(--vx-tap);
    padding: 0.5rem 0.65rem;
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    font-family: inherit;
    font-size: 16px;
}

.vx-field input::placeholder {
    color: #5c6878;
}

.vx-field input:focus-visible,
.vx-field select:focus-visible {
    outline: 2px solid var(--vx-accent);
    outline-offset: -1px;
    border-color: var(--vx-accent);
}

.vx-row {
    display: flex;
    gap: 0.85rem;
    flex-wrap: wrap;
}

.vx-row > .vx-field {
    flex: 1 1 12rem;
}

.vx-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    min-height: var(--vx-tap);
    background: var(--vx-accent);
    border: 1px solid var(--vx-accent);
    border-radius: var(--vx-radius);
    color: #fff;
    padding: 0.45rem 1rem;
    font-family: inherit;
    font-size: 0.95rem;
    line-height: 1.2;
    text-align: center;
    cursor: pointer;
    touch-action: manipulation;
}

.vx-button:hover:not(:disabled) {
    background: var(--vx-accent-hover);
    text-decoration: none;
}

.vx-button:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.vx-button.vx-secondary {
    background: var(--vx-surface-raised);
    border-color: var(--vx-border);
    color: var(--vx-text);
}

.vx-button.vx-secondary:hover:not(:disabled) {
    border-color: var(--vx-accent);
    background: var(--vx-surface-raised);
}

/* Still a full-height target — only the label and the side padding are compact. Shrinking the box
   itself would put a 30px button on a screen operated with a thumb. */
.vx-button.vx-icon {
    padding: 0.3rem 0.65rem;
    font-size: 0.88rem;
}

.vx-message {
    padding: 0.6rem 0.8rem;
    border-radius: var(--vx-radius);
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

.vx-message.vx-error {
    background: rgba(248, 81, 73, 0.12);
    border: 1px solid var(--vx-danger);
    color: #ffb3ae;
}

.vx-message.vx-ok {
    background: rgba(63, 185, 80, 0.12);
    border: 1px solid var(--vx-ok);
    color: #9ae6a8;
}

.vx-empty {
    color: var(--vx-text-muted);
    padding: 2.5rem 1rem;
    text-align: center;
}

.vx-empty p {
    margin: 0 0 1rem;
}

.vx-empty p:last-child {
    margin-bottom: 0;
}

.vx-discovered-list {
    margin-top: 1rem;
}

.vx-discovered {
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    padding: 0.85rem;
    margin-bottom: 0.75rem;
    background: var(--vx-bg);
}

.vx-discovered:last-child {
    margin-bottom: 0;
}

.vx-discovered h3 {
    margin: 0 0 0.15rem;
    font-size: 0.95rem;
}

.vx-discovered .vx-meta {
    color: var(--vx-text-muted);
    font-size: 0.82rem;
    margin-bottom: 0.6rem;
    word-break: break-all;
}

.vx-note {
    color: var(--vx-text-muted);
    font-size: 0.82rem;
}

.vx-note-spaced {
    margin-top: 1rem;
}

/* Recording controls ----------------------------------------------------- */

.vx-tile-footer {
    border-top: 1px solid var(--vx-border);
    padding: 0.5rem 0.75rem 0.6rem;
}

.vx-record {
    display: flex;
    align-items: center;
    gap: 0.5rem 0.75rem;
    flex-wrap: wrap;
}

.vx-record-link {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    min-height: var(--vx-tap);
    font-size: 0.85rem;
}

/* "Keep 7 days" is one decision, so it is drawn as one control rather than as a label, a box and a
   loose word that wrap away from each other the moment the card gets narrow. */

.vx-record-retention {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: var(--vx-tap);
    padding: 0 0.6rem;
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    font-size: 0.85rem;
    color: var(--vx-text-muted);
    white-space: nowrap;
}

.vx-record-retention input {
    /* Stretched to the group's full height so the number itself is the 44px target, rather than a
       28px box floating inside one. */
    align-self: stretch;
    width: 3.5rem;
    padding: 0.2rem 0;
    background: transparent;
    border: none;
    color: var(--vx-text);
    font-family: inherit;
    font-size: 16px;
    text-align: right;
}

.vx-record-retention:focus-within {
    border-color: var(--vx-accent);
    box-shadow: 0 0 0 2px rgba(47, 129, 247, 0.4);
}

.vx-record-retention input:focus-visible {
    outline: none;
}

.vx-record-retention input:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.vx-record-default {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 2.25rem;
    margin-top: 0.2rem;
    font-size: 0.82rem;
    color: var(--vx-text-muted);
    cursor: pointer;
    touch-action: manipulation;
}

.vx-record-default input[type="checkbox"] {
    flex: none;
    width: 1.15rem;
    height: 1.15rem;
    accent-color: var(--vx-accent);
}

.vx-record-note,
.vx-record-error {
    margin: 0.45rem 0 0;
}

.vx-record-error {
    color: var(--vx-danger);
}

/* A switch rather than a bare checkbox: whether a camera is being kept is the one setting on the
   tile that an operator should be able to read from across the room. */

.vx-switch {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: var(--vx-tap);
    cursor: pointer;
    user-select: none;
    touch-action: manipulation;
}

.vx-switch input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.vx-switch-track {
    width: 2.1rem;
    height: 1.1rem;
    flex: none;
    border-radius: 0.55rem;
    background: var(--vx-surface-raised);
    border: 1px solid var(--vx-border);
    position: relative;
    transition: background 0.12s ease, border-color 0.12s ease;
}

.vx-switch-thumb {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 0.85rem;
    height: 0.85rem;
    border-radius: 50%;
    background: var(--vx-text-muted);
    transition: transform 0.12s ease, background 0.12s ease;
}

.vx-switch input:checked + .vx-switch-track {
    background: rgba(248, 81, 73, 0.25);
    border-color: var(--vx-danger);
}

.vx-switch input:checked + .vx-switch-track .vx-switch-thumb {
    transform: translateX(1rem);
    background: var(--vx-danger);
}

.vx-switch input:focus-visible + .vx-switch-track {
    outline: 2px solid var(--vx-accent);
    outline-offset: 2px;
}

.vx-switch input:disabled ~ .vx-switch-track,
.vx-switch input:disabled ~ .vx-switch-text {
    opacity: 0.55;
}

.vx-switch input:disabled {
    cursor: not-allowed;
}

.vx-switch-text {
    font-size: 0.85rem;
}

/* Playback --------------------------------------------------------------- */

.vx-playback-head {
    display: flex;
    align-items: baseline;
    gap: 0.35rem 1rem;
    flex-wrap: wrap;
    margin-bottom: 0.5rem;
}

.vx-playback-head h2 {
    flex: 1 1 14rem;
    margin: 0;
    font-size: 1.15rem;
}

.vx-playback-head a {
    display: inline-flex;
    align-items: center;
    min-height: var(--vx-tap);
    font-size: 0.9rem;
    white-space: nowrap;
}

.vx-daybar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin: 0.85rem 0;
}

.vx-daybar input[type="date"] {
    min-height: var(--vx-tap);
    padding: 0.25rem 0.6rem;
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    font-family: inherit;
    font-size: 16px;
}

/* On a phone the day being read is the heading of this screen, so it gets its own full-width row and
   the three ways of moving off it divide the row underneath evenly. Four controls on one line at
   this width is how "Previous day" ends up alone on a line of its own. */

@media (max-width: 640px) {
    .vx-daybar {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 0.5rem;
    }

    .vx-daybar > input[type="date"] {
        grid-row: 1;
        grid-column: 1 / -1;
        width: 100%;
    }

    .vx-daybar > button:nth-of-type(1) {
        grid-row: 2;
        grid-column: 1;
    }

    .vx-daybar > button:nth-of-type(2) {
        grid-row: 2;
        grid-column: 2;
    }

    .vx-daybar > button:nth-of-type(3) {
        grid-row: 2;
        grid-column: 3;
    }

    .vx-daybar .vx-button.vx-icon {
        padding: 0.3rem 0.35rem;
        font-size: 0.82rem;
    }
}

.vx-vod {
    max-width: 60rem;
    margin-bottom: 1rem;
}

.vx-vod .vx-stage {
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    overflow: hidden;
}

.vx-vod video {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: contain;
    background: #000;
}

.vx-vod-status {
    font-size: 0.82rem;
    color: var(--vx-text-muted);
    padding: 0.4rem 0.1rem 0;
}

.vx-vod-status[data-state="error"],
.vx-vod-status[data-state="stalled"] {
    color: var(--vx-danger);
}

/* The day strip. Blocks are placed by time of day, so a gap on screen is a gap in the recording —
   which is the whole reason this screen exists. */

/* The strip is held in a scroller with a floor under its width. Squeezing a whole day into 320px
   would push the hour labels into each other, and a strip whose hours cannot be read is not a
   timeline — it is a pattern. Below the floor it scrolls instead of compressing. */

.vx-timeline-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    margin: 0.5rem 0 0.25rem;
    padding-bottom: 0.15rem;
}

.vx-timeline {
    position: relative;
    /* Tall enough for a 44px block with the hour labels clear underneath rather than against them. */
    height: 4.75rem;
    min-width: 21rem;
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    overflow: hidden;
}

.vx-timeline-tick {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--vx-border);
}

.vx-timeline-tick span {
    position: absolute;
    bottom: 0.25rem;
    left: 0.25rem;
    font-size: 0.68rem;
    font-variant-numeric: tabular-nums;
    color: var(--vx-text-muted);
}

/* 44px tall: on the wall these are how an operator picks a clip, with a thumb. */
.vx-timeline-block {
    position: absolute;
    top: 0.4rem;
    height: var(--vx-tap);
    min-width: 3px;
    padding: 0;
    background: var(--vx-accent);
    border: none;
    border-radius: 3px;
    cursor: pointer;
    touch-action: manipulation;
}

.vx-timeline-block:hover {
    background: var(--vx-accent-hover);
}

.vx-timeline-block.vx-selected {
    background: var(--vx-ok);
    outline: 1px solid var(--vx-text);
}

.vx-segments {
    display: grid;
    gap: 0.4rem;
    grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
    margin-top: 0.85rem;
}

.vx-segment {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 0.1rem;
    min-height: var(--vx-tap);
    text-align: left;
    padding: 0.45rem 0.7rem;
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-left: 3px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    font-family: inherit;
    font-size: 0.95rem;
    cursor: pointer;
    touch-action: manipulation;
}

.vx-segment:hover {
    border-color: var(--vx-accent);
}

.vx-segment.vx-selected {
    border-color: var(--vx-ok);
    background: var(--vx-surface-raised);
}

.vx-segment-time {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.vx-segment-meta {
    font-size: 0.78rem;
    color: var(--vx-text-muted);
}

/* ---- Camera management -------------------------------------------------------------------
   The list page is deliberately plain: it opens no streams, so it stays usable on exactly the
   camera that is refusing to connect, which is when an operator needs it most. */

.vx-page-title {
    margin: 0 0 0.9rem;
    font-size: 1.25rem;
    font-weight: 600;
}

.vx-camera-list {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.vx-camera-card {
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius-lg);
    box-shadow: var(--vx-shadow);
    padding: 1rem;
}

.vx-camera-head {
    display: flex;
    align-items: center;
    gap: 0.4rem 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.6rem;
}

.vx-camera-name {
    font-weight: 600;
    font-size: 1.02rem;
    margin-right: 0.15rem;
}

.vx-badge {
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--vx-border);
    border-radius: 999px;
    padding: 0.1rem 0.5rem;
    font-size: 0.72rem;
    line-height: 1.35;
    white-space: nowrap;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--vx-text-muted);
}

/* Registration and liveness are different facts, so "unknown" gets its own treatment rather than
   borrowing the one for offline. A dashed grey badge says we do not know; a red one says we do. */
.vx-badge.vx-reg[data-state="online"] {
    color: var(--vx-ok);
    border-color: var(--vx-ok);
}

.vx-badge.vx-reg[data-state="offline"] {
    color: var(--vx-danger);
    border-color: var(--vx-danger);
}

.vx-badge.vx-reg[data-state="unregistered"] {
    color: var(--vx-warn);
    border-color: var(--vx-warn);
}

.vx-badge.vx-reg[data-state="unknown"] {
    color: var(--vx-text-muted);
    border-style: dashed;
}

.vx-camera-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.15rem 0.75rem;
    margin: 0 0 0.85rem;
    font-size: 0.85rem;
}

.vx-camera-facts dt {
    color: var(--vx-text-muted);
}

.vx-camera-facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

.vx-camera-actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* Four verbs and a phone. Left to wrap they come out as three and then one, which is the ragged
   edge the operator was looking at; a two-by-two grid gives the same four buttons one shape. */

@media (max-width: 640px) {
    .vx-camera-actions {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.5rem;
    }

    .vx-camera-card {
        padding: 0.85rem;
    }

    /* An RTSP URL is longer than a phone is wide, so the label goes above the value rather than
       stealing a column from it. */
    .vx-camera-facts {
        grid-template-columns: 1fr;
        gap: 0 0;
    }

    .vx-camera-facts dt {
        margin-top: 0.5rem;
        font-size: 0.78rem;
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }

    .vx-camera-facts dt:first-of-type {
        margin-top: 0;
    }

    /* Row one is what is being kept and where to read it; row two is for how long. Three controls
       across a phone is what made this line wrap into a staircase. */
    .vx-record {
        display: grid;
        grid-template-columns: 1fr auto;
        align-items: center;
        gap: 0.4rem 0.75rem;
    }

    .vx-record > .vx-switch {
        grid-column: 1;
        grid-row: 1;
    }

    .vx-record > .vx-record-retention {
        grid-column: 1 / -1;
        grid-row: 2;
        justify-self: start;
    }

    .vx-record > .vx-record-link {
        grid-column: 2;
        grid-row: 1;
        margin-left: 0;
        justify-self: end;
    }
}

.vx-button.vx-danger {
    background: transparent;
    border-color: var(--vx-danger);
    color: var(--vx-danger);
}

.vx-button.vx-danger:hover:not(:disabled) {
    background: var(--vx-danger);
    color: var(--vx-bg);
}

.vx-result {
    margin-top: 0.75rem;
    padding: 0.6rem 0.75rem;
    border-radius: var(--vx-radius);
    border: 1px solid var(--vx-border);
    background: var(--vx-surface-raised);
}

.vx-result[data-state="ok"] {
    border-color: var(--vx-ok);
}

.vx-result[data-state="bad"] {
    border-color: var(--vx-warn);
}

.vx-result-summary {
    margin: 0 0 0.25rem;
}

.vx-result p:last-child {
    margin-bottom: 0;
}

.vx-edit {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--vx-border);
}

.vx-confirm {
    margin-top: 0.75rem;
    padding: 0.75rem;
    border-radius: var(--vx-radius);
    border: 1px solid var(--vx-danger);
    background: var(--vx-surface-raised);
    font-size: 0.88rem;
}

.vx-confirm-lead {
    margin: 0 0 0.4rem;
}

.vx-confirm p {
    margin: 0 0 0.6rem;
}

.vx-camera-recording {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--vx-border);
}

/* The tile has no padding of its own — the picture runs to its edges — so anything that opens
   inside it has to bring its own. */
.vx-tile > .vx-confirm {
    margin: 0 0.7rem 0.7rem;
}

/* ---- What is left of the phone -----------------------------------------------------------------
   Panels and confirmations lose a little of their padding at this width, for the same reason the
   page gutter does: on a 393px screen a 20px inset on a card inside a 20px page inset spends a
   fifth of the display on nothing. */

@media (max-width: 640px) {
    .vx-panel {
        padding: 0.9rem;
    }

    .vx-confirm {
        padding: 0.7rem;
    }

    .vx-tile-footer {
        padding: 0.5rem 0.7rem 0.65rem;
    }
}

/* ---- Accounts, owners and issued camera identities (M4) ----------------------------------------
   Added rather than reworked: everything below is new surface — the signed-in badge in the header,
   the user list, the owner line on a camera card, and the one-time credential ticket. The phone-first
   sizing above applies to all of it because it is built from the same panels, fields and badges. */

/* A third message colour. The Users page needs to say "somebody is waiting" without saying it in the
   red reserved for things that have gone wrong. */
.vx-message.vx-warn {
    background: rgba(210, 153, 34, 0.12);
    border: 1px solid var(--vx-warn);
    color: #f0cf7a;
}

/* Who is signed in, in the header. It sits after the nav rail rather than inside it: the rail
   scrolls on a phone, and the way out of an account you did not mean to be in must not scroll away. */
.vx-who {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
}

.vx-who-name {
    color: var(--vx-text-muted);
    font-size: 0.85rem;
    max-width: 9rem;
/* ---- Events ------------------------------------------------------------------------------
   Written for the phone first, because that is where this page is read: an operator asking what
   happened at the back gate last night is standing in a hallway, not sitting at a console. Every
   control here is at least 44px on its shortest side — the size a thumb can hit without aiming —
   and the row is a stack that becomes a table only when there is width to spare. */

.vx-eventbar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: flex-end;
    margin-bottom: 1rem;
}

.vx-eventbar-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    flex: 1 1 10rem;
    font-size: 0.85rem;
    color: var(--vx-text-muted);
}

.vx-eventbar-field select,
.vx-eventbar-field input[type="date"] {
    min-height: 44px;
    padding: 0.5rem 0.65rem;
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    font: inherit;
}

.vx-eventbar-field select:focus,
.vx-eventbar-field input[type="date"]:focus {
    outline: 2px solid var(--vx-accent);
    outline-offset: -1px;
}

.vx-eventbar-buttons {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.vx-eventbar-buttons .vx-button {
    min-height: 44px;
    min-width: 44px;
}

.vx-events {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.vx-event {
    display: grid;
    grid-template-columns: max-content 1fr;
    grid-template-areas:
        "time camera"
        "what what";
    gap: 0.15rem 0.75rem;
    align-items: baseline;
    padding: 0.5rem 0.75rem;
    min-height: 44px;
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-left: 3px solid var(--vx-border);
    border-radius: var(--vx-radius);
}

/* Wide enough for one line: time, camera, and what happened, in that order — the order an operator
   scans in when they already know roughly when. */
@media (min-width: 40rem) {
    .vx-event {
        grid-template-columns: max-content 12rem 1fr;
        grid-template-areas: "time camera what";
        align-items: center;
    }
}

.vx-event-time {
    grid-area: time;
    font-variant-numeric: tabular-nums;
    color: var(--vx-text);
}

.vx-event-time small {
    color: var(--vx-text-muted);
    margin-left: 0.4rem;
    font-size: 0.78rem;
}

.vx-event-camera {
    grid-area: camera;
    color: var(--vx-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.vx-user-list {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.vx-user {
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    padding: 0.85rem;
    background: var(--vx-bg);
}

.vx-user-head {
    display: flex;
    align-items: center;
    gap: 0.4rem 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.3rem;
}

.vx-user-name {
    font-weight: 600;
    font-size: 0.98rem;
}

.vx-user .vx-meta {
    color: var(--vx-text-muted);
    font-size: 0.82rem;
    margin-bottom: 0.6rem;
}

/* The owner line on a camera card, and the picker an administrator assigns one with. */
.vx-owner {
.vx-event-what {
    grid-area: what;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--vx-border);
    font-size: 0.85rem;
    color: var(--vx-text-muted);
}

.vx-owner select {
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    font-size: 0.9rem;
    min-height: var(--vx-tap);
    padding: 0.4rem 0.6rem;
}

.vx-owner-none {
    color: var(--vx-warn);
}

/* The claim ticket: a camera's issued identity and password, shown exactly once.
   Loud on purpose — this is the only moment the password exists in a readable form, and an installer
   who scrolls past it has to have the identity revoked and reissued. */
.vx-ticket {
    margin-top: 1rem;
    border: 1px solid var(--vx-accent);
    border-radius: var(--vx-radius);
    background: var(--vx-surface-raised);
    padding: 0.9rem;
}

.vx-ticket h3 {
    margin: 0 0 0.5rem;
    font-size: 0.95rem;
}

.vx-ticket dl {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.3rem 0.9rem;
    margin: 0 0 0.6rem;
    font-size: 0.9rem;
}

.vx-ticket dt {
    color: var(--vx-text-muted);
}

.vx-ticket dd {
    margin: 0;
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    word-break: break-all;
    -webkit-user-select: all;
    user-select: all;
}

.vx-identity-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-top: 0.75rem;
}

.vx-identity {
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    padding: 0.7rem;
    background: var(--vx-bg);
    font-size: 0.88rem;
}

.vx-identity .vx-identity-id {
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    word-break: break-all;
}

.vx-identity .vx-camera-actions {
    margin-top: 0.5rem;
}

@media (max-width: 640px) {
    /* At this width the header is already two rows; a name column competing with the nav rail for
       the same line is what pushes Sign out off the screen. */
    .vx-who-name {
        max-width: 6rem;
    }

    .vx-ticket dl {
        grid-template-columns: 1fr;
        gap: 0.1rem;
    }

    .vx-ticket dt {
        margin-top: 0.4rem;
}

.vx-event-raw {
    font-size: 0.68rem;
    text-transform: none;
    letter-spacing: 0;
    word-break: break-all;
}

/* The stripe carries the kind. Colour is never the only signal — the row says what happened in
   words — so a reader who cannot separate these still reads the same list. */

.vx-event[data-kind="motion"] { border-left-color: var(--vx-accent); }
.vx-event[data-kind="human"],
.vx-event[data-kind="face"] { border-left-color: var(--vx-warn); }
.vx-event[data-kind="intrusion"],
.vx-event[data-kind="line"],
.vx-event[data-kind="tamper"] { border-left-color: var(--vx-danger); }
.vx-event[data-kind="vehicle"] { border-left-color: var(--vx-ok); }

/* An ended state is history; a running one is not. The list dims what is over rather than hiding
   it, because the pair is what tells an operator how long something lasted. */
.vx-event[data-active="false"] .vx-event-what {
    color: var(--vx-text-muted);
}

/* Live badge on a wall tile ----------------------------------------------- */

.vx-live-event {
    flex: none;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    font-size: 0.72rem;
    letter-spacing: 0.02em;
    white-space: nowrap;
    color: var(--vx-bg);
    background: var(--vx-warn);
    animation: vx-live-pulse 2s ease-in-out infinite;
}

.vx-live-event[data-kind="motion"] { background: var(--vx-accent); color: #fff; }
.vx-live-event[data-kind="intrusion"],
.vx-live-event[data-kind="line"],
.vx-live-event[data-kind="tamper"] { background: var(--vx-danger); color: #fff; }

@keyframes vx-live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

/* A pulsing badge on a wall of them is a lot of motion for anybody who finds it hard to look at.
   The colour still says what the words say. */
@media (prefers-reduced-motion: reduce) {
    .vx-live-event {
        animation: none;
    }
}
