/* 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;
}

/* The notification switch, on its own line under the recording row rather than crowded into it.
   Two switches side by side read as one setting with two halves, and these are separate questions:
   one is about a disk and one is about somebody's pocket. */
.vx-notify {
    display: flex;
    align-items: center;
    gap: 0.5rem 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.35rem;
}

/* The microphone switch, under the other two and with its sentence attached rather than in a
   tooltip. Recording and notifications are decisions about the operator's own disk and pocket; this
   one is a decision about everybody who walks in front of the camera, and the one line explaining
   that has to be visible without hovering — which on the phone this console is also built for is not
   a thing anybody can do. */
.vx-mic {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin-top: 0.35rem;
}

.vx-mic-note {
    margin: 0;
    max-width: 46ch;
}

/* On a wall tile: state, never a control. It says this camera is recording sound, and the way to
   change that is the switch on the camera's own card. A glyph that looked pressable here would be
   the second place a microphone can be turned on, and two of those is how one gets left on. */
.vx-tile-mic {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background: color-mix(in srgb, var(--vx-bg) 70%, transparent);
    color: var(--vx-text);
    font-size: 0.8rem;
    line-height: 1;
    flex: none;
    pointer-events: none;
}

.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);
}

/* What a recorded camera records, on the line under the switch that turned it on.

   A segmented row rather than the stacked rows .vx-choice uses, and the difference is label length:
   those labels are sentences, these are one and two words, so a row cannot put a control under the
   wrong label the way a wrapped row of sentence-radios can. It is drawn only when the switch is on,
   because "how much" is not a question until "any" has been answered. */

.vx-record-mode {
    display: inline-flex;
    margin-top: 0.4rem;
    padding: 2px;
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
}

.vx-record-mode .vx-seg {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.25rem;
    padding: 0 0.75rem;
    border-radius: calc(var(--vx-radius) - 2px);
    font-size: 0.82rem;
    line-height: 1.2;
    white-space: nowrap;
    color: var(--vx-text-muted);
    cursor: pointer;
    user-select: none;
    touch-action: manipulation;
    transition: background 0.12s ease, color 0.12s ease;
}

.vx-record-mode .vx-seg input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

/* The selected segment carries the same red the switch does, because it means the same thing: this
   camera is writing pictures of somebody's home to a disk. Which of the two it is changes how much
   gets written, not what is happening. */
.vx-record-mode .vx-seg[data-selected="true"] {
    background: rgba(248, 81, 73, 0.2);
    color: var(--vx-text);
    box-shadow: inset 0 0 0 1px var(--vx-danger);
}

.vx-record-mode .vx-seg:hover:not([data-selected="true"]) {
    color: var(--vx-text);
}

.vx-record-mode .vx-seg input:focus-visible + span {
    outline: 2px solid var(--vx-accent);
    outline-offset: 3px;
    border-radius: 2px;
}

.vx-record-mode .vx-seg input:disabled {
    cursor: not-allowed;
}

.vx-record-mode:has(input:disabled) {
    opacity: 0.55;
}

/* 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);
}

/* --- The camera's own card ---------------------------------------------------------------------

   Two stores, drawn so they cannot be mistaken for each other. What this server holds is the accent
   colour, solid. What only exists on the camera's card is a different hue with a 45-degree hatch
   over it — deliberately not a lighter shade of the same colour, because a shade difference is
   exactly what disappears on a dimmed laptop screen in a back office, and being wrong about which
   store a recording is in is being wrong about whether it can be downloaded, shared, or relied on to
   still be there tomorrow. The vehicle console draws the same distinction the same way. */

.vx-card-timeline {
    /* A dashed edge on the track itself, so the two strips differ before a single band is drawn. */
    border-style: dashed;
}

.vx-card-block {
    background: var(--vx-warn);
    background-image: repeating-linear-gradient(
        45deg,
        rgba(255, 255, 255, 0.22) 0 5px,
        rgba(255, 255, 255, 0) 5px 10px);
}

.vx-card-block:hover {
    background: var(--vx-warn);
    filter: brightness(1.2);
}

.vx-card-block.vx-selected {
    outline: 2px solid var(--vx-text);
}

.vx-card-key {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.vx-card-swatch {
    display: inline-block;
    width: 1.1rem;
    height: 0.75rem;
    border-radius: 2px;
    flex: none;
}

.vx-card-swatch[data-kind="server"] {
    background: var(--vx-accent);
}

.vx-card-swatch[data-kind="card"] {
    background: var(--vx-warn);
    background-image: repeating-linear-gradient(
        45deg,
        rgba(255, 255, 255, 0.22) 0 5px,
        rgba(255, 255, 255, 0) 5px 10px);
}

.vx-card-panel {
    margin-top: 1.25rem;
}

.vx-card-player {
    margin: 0.85rem 0;
    max-width: 60rem;
}

.vx-card-player-frame {
    position: relative;
    background: #000;
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    overflow: hidden;
}

.vx-card-player-frame video {
    display: block;
    width: 100%;
    /* A card recording is 4:3 as often as it is 16:9, so the box is given a ratio and the picture is
       fitted inside it rather than the box being stretched to whatever arrives. */
    aspect-ratio: 16 / 9;
    object-fit: contain;
    background: #000;
}

.vx-card-player-status {
    position: absolute;
    left: 0.5rem;
    bottom: 0.5rem;
    max-width: calc(100% - 1rem);
    padding: 0.25rem 0.5rem;
    border-radius: var(--vx-radius);
    background: rgba(0, 0, 0, 0.72);
    color: var(--vx-text);
    font-size: 0.8rem;
}

.vx-card-player-status.vx-error {
    color: var(--vx-danger);
}

.vx-card-player-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.5rem;
}

.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);
}

/* ---- Keeping and sharing a segment -------------------------------------------------------
   Placed here, among the segment rules it belongs with, rather than at the end of the file. That
   is not preference: a merge left two rules further down unclosed, so everything after them is
   swallowed by the block above it. Rules added at the end of this file do not apply. Repairing
   that damage is its own change; this one stays on the working side of it. */

/* A segment row: the clip on the left, what can be done with it on the right. On a phone the
   actions drop underneath rather than squeezing the time out of the row, because the time is how
   an operator finds the clip and the buttons are what they do once they have. */
.vx-segment-row {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 0.4rem;
}

.vx-segment-row > .vx-segment {
    flex: 1 1 12rem;
}

.vx-segment-row.vx-selected > .vx-segment {
    border-color: var(--vx-ok);
    background: var(--vx-surface-raised);
}

.vx-segment-actions {
    display: flex;
    align-items: stretch;
    gap: 0.3rem;
    flex: 0 0 auto;
}

.vx-segment-action {
    min-height: var(--vx-tap);
    padding: 0.35rem 0.7rem;
    font-size: 0.82rem;
    white-space: nowrap;
}

/* The share panel. It sits under the player because it is about the clip that is playing, and it
   is a panel rather than a dialog because there is nothing here worth trapping focus for — the
   decision is "how long", and the answer is three options. */
.vx-share-panel,
.vx-share-list-panel {
    margin-top: 1rem;
}

/* The link, once. Loud for the same reason the camera credential ticket is: this is the only moment
   the token exists in a readable form, and somebody who scrolls past it has to revoke and start
   again. It does not reuse .vx-ticket, which sits in a part of this file a bad merge left unclosed
   and which therefore currently applies to nothing. */
.vx-share-ticket {
    margin-top: 0;
    border: 1px solid var(--vx-accent);
    border-radius: var(--vx-radius);
    background: var(--vx-surface-raised);
    padding: 0.9rem;
}

.vx-share-ticket h3 {
    margin: 0 0 0.6rem;
    font-size: 0.95rem;
}

/* An input rather than a paragraph, so the link can be selected and copied by hand on the browsers
   and the insecure origins where the clipboard API refuses. */
.vx-share-url {
    width: 100%;
    min-height: var(--vx-tap);
    padding: 0.4rem 0.6rem;
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.82rem;
}

.vx-share-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.6rem;
}

/* The sentence that says a link will outlive its clip. Warn rather than danger: nothing has gone
   wrong, and something is about to be true that the operator would otherwise learn a week later. */
.vx-share-warning {
    color: #f0cf7a;
}

.vx-share-list {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.vx-share-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    min-height: var(--vx-tap);
    background: var(--vx-surface);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
}

.vx-share-what {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
}

.vx-share-clip {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.vx-share-note {
    font-style: italic;
    overflow-wrap: anywhere;
}

/* ---- 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;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- 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 {
    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-event-what {
    grid-area: what;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.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);
}

/* One of a small set of alternatives, each on its own row. Rows rather than a line of radios
   because the labels are sentences ("Generate one for me") rather than words, and a wrapped row of
   radios puts a control under the wrong label on a narrow screen. */
.vx-choice {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 2rem;
    font-size: 0.85rem;
    font-weight: 400;
    color: var(--vx-text);
    cursor: pointer;
    touch-action: manipulation;
}

.vx-choice input[type="radio"] {
    flex: none;
    width: 1.1rem;
    height: 1.1rem;
    accent-color: var(--vx-accent);
}

/* 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;
    }
}

/* --- A camera's own settings -------------------------------------------------------------------

   Phone first, like everything else here: a slider and its number sit on one row that wraps rather
   than shrinks, because a range input squeezed under 8rem cannot be dragged with a thumb at all. */

.vx-knob {
    margin-bottom: 1rem;
}

.vx-knob label {
    display: block;
    margin-bottom: 0.35rem;
    font-size: 0.85rem;
    color: var(--vx-text-muted);
}

.vx-knob-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.vx-knob-row input[type="range"] {
    flex: 1 1 10rem;
    min-width: 8rem;

    /* A range input's own height is about 20px, which is not a target. The padding is what makes the
       whole strip catchable by a thumb that lands anywhere near it. */
    min-height: var(--vx-tap);
    accent-color: var(--vx-accent);
    background: none;
    border: none;
    padding: 0;
    touch-action: pan-y;
}

.vx-knob-value {
    flex: none;
    min-width: 3.25rem;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    background: var(--vx-bg);
    font-variant-numeric: tabular-nums;
    text-align: right;
    font-size: 0.95rem;
}

/* The stated range, sitting inside a label. Muted because it is the camera's constraint rather than
   the operator's setting, and it must not compete with the name of the thing being set. */
.vx-note-inline {
    color: var(--vx-text-muted);
    font-size: 0.8rem;
    font-weight: normal;
}

/* One line per setting a write asked for, saying what the camera kept. Not a table: on a phone a
   two-column table of numbers wraps into something nobody can read, and these are sentences. */
.vx-outcomes {
    margin: 0.5rem 0 0;
    padding-left: 1.1rem;
    font-size: 0.9rem;
}

.vx-outcomes li {
    margin-bottom: 0.25rem;
}

/* A setting the camera clamped is called out, because it is the one line on the page an operator
   has to read. Colour alone would not do it — the sentence already says "asked for X, kept Y". */
.vx-outcomes li[data-accepted="false"] {
    color: var(--vx-warn);
}

.vx-panel h2 .vx-badge {
    margin-left: 0.5rem;
    vertical-align: middle;
}

/* The settings link on a camera card. A link rather than a button because it navigates, and a
   button that navigates breaks the browser's own back, middle-click and open-in-new-tab. */
.vx-camera-actions a.vx-button {
    text-decoration: none;
}

/* ---- Storage plans and usage -----------------------------------------------------------------

   The plan list borrows the account list's shape rather than inventing one: both are a short list
   of named things with two facts and a couple of buttons, and on a phone both are a stack of cards
   with nothing to align. */
.vx-plan-list {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.vx-plan {
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    padding: 0.85rem;
    background: var(--vx-bg);
}

.vx-plan-head {
    display: flex;
    align-items: center;
    gap: 0.4rem 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.3rem;
}

.vx-plan-name {
    font-weight: 600;
    font-size: 0.98rem;
}

.vx-plan .vx-meta {
    color: var(--vx-text-muted);
    font-size: 0.82rem;
    margin-bottom: 0.6rem;
}

/* The plan picker on an account's row. Its own class rather than reusing .vx-owner because that
   one carries a top border meaning "a different kind of fact starts here", and two of those in a
   row reads as a rule somebody forgot to remove. */
.vx-plan-pick {
    margin: 0.6rem 0;
    max-width: 22rem;
}

.vx-plan-pick select {
    background: var(--vx-bg);
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    color: var(--vx-text);
    min-height: var(--vx-tap);
    padding: 0.4rem 0.6rem;
    width: 100%;
}

/* One account's storage. The numbers are the point and the bar is the shortcut, so the numbers sit
   above the bar rather than inside it — text laid over a fill changes contrast as the fill moves
   past it, which is exactly the moment it most needs to be readable. */
.vx-usage {
    margin: 0.75rem 0;
    padding: 0.75rem;
    border: 1px solid var(--vx-border);
    border-radius: var(--vx-radius);
    background: var(--vx-surface-raised);
}

.vx-usage-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.vx-usage-plan {
    font-weight: 600;
    font-size: 0.9rem;
}

.vx-usage-numbers {
    color: var(--vx-text-muted);
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
}

.vx-usage-bar {
    margin-top: 0.5rem;
    height: 0.5rem;
    border-radius: 999px;
    background: var(--vx-border);
    overflow: hidden;
}

.vx-usage-fill {
    display: block;
    height: 100%;
    background: var(--vx-accent);
    border-radius: inherit;

    /* Not zero-width when the account has used nothing at all: a bar that disappears reads as a bar
       that failed to draw. */
    min-width: 2px;
    transition: width 0.2s ease;
}

/* Over the allowance, and the fill is the whole bar because the fraction is past one. The colour
   is the only thing that distinguishes it from exactly full, so the sentence underneath is what
   actually carries the meaning — see .vx-usage[data-over="true"] .vx-note below. */
.vx-usage[data-over="true"] .vx-usage-fill {
    background: var(--vx-warn);
}

.vx-usage .vx-note {
    margin-bottom: 0;
}

/* The over-quota fact line, wherever it is drawn: on the storage page, on an administrator's
   account list, and across the top of the wall and the camera list. A statement of what is
   happening rather than a warning to act on — recording did not stop, and there is nothing to do
   in a hurry. */
.vx-message.vx-fact {
    border-color: var(--vx-warn);
    background: rgba(210, 153, 34, 0.12);
    color: var(--vx-text);
}

.vx-message.vx-fact a {
    color: var(--vx-warn);
    text-decoration: underline;
}

/* Phone first, like everything else here. The plan name and the two numbers sit on one row while
   there is room and stack when there is not — an allowance rendered as "250 M" because it was
   squeezed is worse than one on a line of its own. */
@media (max-width: 640px) {
    .vx-usage-head {
        display: block;
    }

    .vx-usage-numbers {
        display: block;
        margin-top: 0.15rem;
    }

    .vx-plan-pick {
        max-width: none;
    }

    /* Two buttons per row, matching the camera card's actions, so a plan row and a camera row do
       not disagree about how wide a button is on the same screen. */
    .vx-plan .vx-camera-actions {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.5rem;
    }
}

/* The bar animates its width so a "measure again" that moved it is visible as a movement rather
   than as a redraw. Somebody who has asked for less motion gets the number instead. */
@media (prefers-reduced-motion: reduce) {
    .vx-usage-fill {
        transition: none;
    }
}

/* ---- Sharing a camera with another account -----------------------------------------------------
   Two sides of one feature, and they never appear together. An owner sees the household — a row of
   chips and a box to add to it. Somebody the camera was shared with sees a badge, a line naming the
   owner, and fewer controls than they would on a camera of their own. */

/* The level chip in a shared camera's heading. Not coloured like the registration badge: that one
   reports a fault or the absence of one, and this reports a fact about the viewer. */
.vx-badge.vx-shared-badge {
    border-style: dashed;
    border-color: var(--vx-accent);
    color: var(--vx-accent);
}

/* Whose camera this is, under the name. Muted, because it is context rather than news — but present
   on every surface the camera appears on, because a wall of pictures from two households with
   nothing distinguishing them is how somebody talks about the wrong one. */
.vx-shared-by {
    margin: -0.35rem 0 0.7rem;
    color: var(--vx-text-muted);
    font-size: 0.82rem;
}

/* The same fact on a live tile, where the space is a single line beside the camera's name. */
.vx-tile-shared {
    flex: none;
    max-width: 45%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--vx-text-muted);
    font-size: 0.78rem;
    font-weight: 400;
}

/* The owner's half: who else can see this camera, on the card rather than on a page of its own.
   Sharing is something somebody does while looking at the camera. */
.vx-shared-with {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--vx-border);
    font-size: 0.85rem;
}

.vx-shared-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}

.vx-shared-label {
    font-weight: 600;
    color: var(--vx-text);
}

/* A list, because it is one — the bullets are off and the semantics stay, so a screen reader says
   how many people this camera is shared with before reading their names. */
.vx-chips {
    list-style: none;
    margin: 0 0 0.7rem;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.vx-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    max-width: 100%;
    padding: 0.25rem 0.3rem 0.25rem 0.6rem;
    border: 1px solid var(--vx-border);
    border-radius: 999px;
    background: var(--vx-surface-raised);
}

.vx-chip-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A grant whose account has been deleted outright. The row is still shown, with its revoke, because
   a grant an owner cannot see is a grant they cannot clear — and it is drawn as a warning rather
   than as an ordinary person, because there is nobody behind it any more. */
.vx-chip[data-missing="yes"] .vx-chip-name {
    color: var(--vx-warn);
    font-style: italic;
}

.vx-chip-level {
    flex: none;
    padding: 0 0.4rem;
    border-radius: 999px;
    background: var(--vx-bg);
    color: var(--vx-text-muted);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* The × on a chip. Round, and the full tap height on a phone, because it is a destructive control
   in a row of small ones and a mis-tap takes somebody's access away. */
.vx-chip-remove {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--vx-text-muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    touch-action: manipulation;
}

.vx-chip-remove:hover:not(:disabled) {
    background: color-mix(in srgb, var(--vx-danger) 20%, transparent);
    color: var(--vx-danger);
}

.vx-chip-remove:disabled {
    opacity: 0.5;
    cursor: default;
}

/* Name, level, button. A row on a desktop and a stack on a phone, where three controls side by side
   would each be too narrow to read what is in them. */
.vx-share-add {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 0.4rem;
}

.vx-share-add .vx-field {
    flex: 1 1 9rem;
    min-width: 0;
    margin: 0;
}

.vx-share-add .vx-field:nth-child(2) {
    flex: 0 1 8rem;
}

.vx-share-add .vx-button {
    flex: none;
}

.vx-share-add select,
.vx-share-add input {
    width: 100%;
    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;
}

/* The grantee's own notification switch, beside the camera's own where both are drawn. Indented a
   little so a Manager looking at two switches can see at a glance that the second is narrower in
   scope than the first. */
.vx-notify-me {
    padding-left: 0.1rem;
}

@media (max-width: 640px) {
    /* One control per row. Three of them side by side on a 393px screen leaves the name box too
       narrow to hold a sign-in name, which is the one thing that has to be readable here. */
    .vx-share-add {
        display: grid;
        grid-template-columns: 1fr;
        align-items: stretch;
    }

    .vx-share-add .vx-field:nth-child(2) {
        flex: none;
    }

    .vx-share-add .vx-button {
        min-height: var(--vx-tap);
    }

    .vx-tile-shared {
        display: none;
    }
}
