/* VP theme - palette lifted verbatim from the approved prototypes
   (vp_admin_console.html / vp_officer_console.html). These are the design source of truth
   for the VP web consoles; keep them in sync. Montserrat is not bundled, so it falls back
   to Segoe UI. */
:root {
    --font: 'Montserrat', 'Segoe UI', system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
    --page-bg: #eef1f5;
    --card: #ffffff;
    --ink: #525f7f;
    --ink-strong: #2f3a54;
    --muted: #8a94a6;
    --line: #e6eaf0;
    --line-soft: #eef1f5;
    --brand: #4a7dfc;
    --brand-ink: #1a2dbc;
    --grad-a: #7b9eea;
    --grad-b: #93b4f0;
    --nav-active-bg: #c2ecfb;
    --nav-active-ink: #4f5dce;
    --thead: #c8d9f0;
    --thead-border: #93b4f0;
    --ok: #2ecc71;
    --ok-d: #118260;
    --warn: #f39c12;
    --danger: #d81b60;
    --cyan: #00acc1;
    --teal: #16a085;
    --shadow: 0 2px 10px rgba(31, 45, 80, .08);
    --shadow-lg: 0 6px 24px rgba(31, 45, 80, .14);
    --radius: 12px;
}

/* ================= console shell: topbar + sidebar + content =================
   Follows the approved admin console prototype. The active-item colours
   (--nav-active-bg / --nav-active-ink) are the prototype's own values.

   x-cloak hides Alpine-controlled elements until Alpine initialises, which stops the mobile
   scrim flashing on first paint. It needs a CSS rule to work at all. */
[x-cloak] {
    display: none !important;
}

.vp-shell {
    margin: 0;
    min-height: 100vh;
    background: var(--page-bg);
    font-family: var(--font);
    color: var(--ink);
}

.vp-topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: 14px;
    height: 58px;
    padding: 0 18px;
    background: var(--card);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow);
}

.vp-topbar__brand {
    display: flex;
    align-items: center;
}

.vp-topbar__toggle {
    display: none;   /* desktop: the sidebar is always visible */
    padding: 6px 10px;
    font-size: 18px;
    line-height: 1;
    color: var(--ink-strong);
    background: none;
    border: 1px solid var(--line);
    border-radius: 8px;
    cursor: pointer;
}

.vp-topbar__user {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-left: auto;
}

.vp-topbar__name {
    font-size: 13px;
    font-weight: 600;
    color: var(--ink-strong);
}

.vp-topbar__action {
    font-size: 13px;
    color: var(--brand);
    text-decoration: none;
}

.vp-topbar__action:hover {
    color: var(--brand-ink);
    text-decoration: underline;
}

.vp-body {
    display: flex;
    align-items: flex-start;
}

.vp-sidebar__wrap {
    flex: 0 0 232px;
    width: 232px;
    align-self: stretch;
    background: var(--card);
    border-right: 1px solid var(--line);
    min-height: calc(100vh - 58px);
}

.vp-sidebar {
    padding: 18px 12px;
    position: sticky;
    top: 58px;
}

/* ---------- sidebar hierarchy ----------
   Two levels, told apart three ways at once rather than by size alone: the heading carries an icon
   the items do not, the items hang off a rail drawn from the heading, and the heading takes the
   active colour when the page you are on lives under it. Any one of those alone was too quiet -
   the uppercase label and the links were reading as one flat list. */
.vp-sidebar__group {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 10px;
    margin: 18px 0 8px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
    transition: color .18s ease;
}

.vp-sidebar__group:first-child {
    margin-top: 0;
}

/* The group you are inside. Colour only - moving or resizing a heading on navigation would make
   the whole sidebar twitch on every page load. */
.vp-sidebar__group.is-current {
    color: var(--nav-active-ink);
}

.vp-sidebar__icon {
    flex: 0 0 16px;
    width: 16px;
    height: 16px;
    opacity: .75;
    transition: opacity .18s ease, transform .18s ease;
}

.vp-sidebar__group.is-current .vp-sidebar__icon {
    opacity: 1;
    transform: scale(1.08);
}

/* The rail. Indented to sit under the heading's TEXT, not its icon, so the eye reads
   icon -> heading -> items as one column rather than three loose things. */
.vp-sidebar__list {
    list-style: none;
    margin: 0 0 4px 13px;
    padding: 0 0 0 10px;
    border-left: 1px solid var(--line);
}

.vp-sidebar__link {
    position: relative;
    display: block;
    padding: 9px 10px;
    margin-bottom: 2px;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--ink);
    text-decoration: none;
    border-radius: 8px;
    transition: background .16s ease, color .16s ease, transform .16s ease;
}

.vp-sidebar__link:hover {
    background: var(--line-soft);
    color: var(--ink-strong);
    transform: translateX(2px);
}

.vp-sidebar__link.is-active {
    background: var(--nav-active-bg);
    color: var(--nav-active-ink);
}

/* The marker sits ON the rail, so the active row is findable from the group's spine even when the
   pill's colour is doing the same job. It grows from the middle on arrival - the one bit of motion
   that runs on page load, and it is 180ms on a 2px sliver. */
.vp-sidebar__link.is-active::before {
    content: "";
    position: absolute;
    left: -11px;
    top: 50%;
    width: 2px;
    height: 0;
    border-radius: 1px;
    background: var(--nav-active-ink);
    transform: translateY(-50%);
    animation: vp-nav-mark .18s ease-out forwards;
}

@keyframes vp-nav-mark {
    to { height: 58%; }
}

/* Nobody who has asked for less motion needs a nav that slides. The marker still appears, at full
   height immediately, because it carries meaning rather than decoration. */
@media (prefers-reduced-motion: reduce) {
    .vp-sidebar__group,
    .vp-sidebar__icon,
    .vp-sidebar__link {
        transition: none;
    }

    .vp-sidebar__link:hover {
        transform: none;
    }

    .vp-sidebar__group.is-current .vp-sidebar__icon {
        transform: none;
    }

    .vp-sidebar__link.is-active::before {
        height: 58%;
        animation: none;
    }
}

.vp-sidebar__empty {
    padding: 10px;
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--muted);
}

.vp-main {
    flex: 1 1 auto;
    min-width: 0;   /* lets wide tables scroll instead of stretching the layout */
    padding: 26px 28px 40px;
}

.vp-scrim {
    position: fixed;
    inset: 58px 0 0;
    z-index: 18;
    background: rgba(31, 45, 80, .35);
}

@media (max-width: 900px) {
    .vp-topbar__toggle {
        display: inline-block;
    }

    .vp-topbar__name,
    .vp-topbar__action {
        display: none;   /* keep the sign-out button reachable on a narrow bar */
    }

    .vp-sidebar__wrap {
        position: fixed;
        top: 58px;
        bottom: 0;
        left: 0;
        z-index: 20;
        transform: translateX(-100%);
        transition: transform .18s ease-out;
        box-shadow: var(--shadow-lg);
    }

    .vp-sidebar__wrap.is-open {
        transform: translateX(0);
    }

    .vp-main {
        padding: 20px 18px 32px;
    }
}

/* ================= schedule calendar =================
   Slot-bubble colours are the prototype's: green for open, pink for full. */
.vp-cal {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 6px;
}

.vp-cal__head {
    padding: 4px 0;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--muted);
    text-align: center;
}

.vp-cal__day {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 56px;
    padding: 6px 4px 9px;
    text-decoration: none;
    color: var(--ink);
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 10px;
    transition: border-color .12s, background .12s;
}

.vp-cal__day:hover {
    border-color: var(--brand);
}

.vp-cal__num {
    font-size: 13px;
    font-weight: 600;
}

.vp-cal__note {
    font-size: 10px;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}

.vp-cal__day.is-today .vp-cal__num {
    color: var(--brand-ink);
    text-decoration: underline;
}

/* The meter: booked seats as a share of capacity, drawn in the day's own colour so it needs no
   legend of its own. Sits along the foot of the cell, inside the padding added above. */
.vp-cal__meter {
    position: absolute;
    left: 8px;
    right: 8px;
    bottom: 4px;
    height: 3px;
    border-radius: 2px;
    background: rgba(47, 58, 84, .08);
    overflow: hidden;
}

.vp-cal__meter > span {
    display: block;
    height: 100%;
    background: currentColor;
    border-radius: 2px;
}

/* ----- the seven day states -----
   Each selector pair styles the cell and its legend swatch together, so the legend cannot drift
   from the grid. The note inherits the cell colour in every coloured state. */

.vp-cal__day.has-slots,
.vp-legend i.has-slots {
    background: #e7f8ef;
    border-color: #8fd9b4;
    color: var(--ok-d);
}

.vp-cal__day.is-partial,
.vp-legend i.is-partial {
    background: #e8efff;
    border-color: #9db8f7;
    color: var(--brand-ink);
}

.vp-cal__day.is-full,
.vp-legend i.is-full {
    background: #fde7ef;
    border-color: #f0a9c6;
    color: #c01860;
}

.vp-cal__day.is-leave,
.vp-legend i.is-leave {
    background: repeating-linear-gradient(135deg, #f3f5f9 0 4px, #e6eaf0 4px 8px);
    border-color: var(--line);
    color: var(--muted);
}

.vp-cal__day.is-unset,
.vp-legend i.is-unset {
    background: #fff;
    border-style: dashed;
    border-color: #c5cfdf;
    color: var(--ink);
}

.vp-cal__day.is-past,
.vp-legend i.is-past {
    background: #fff;
    border-color: var(--line-soft);
    color: var(--muted);
}

.vp-cal__day.is-past .vp-cal__num {
    font-weight: 500;
}

/* Beyond the VP period: still a day of the month, so still drawn, but nothing can happen there. */
.vp-cal__day.is-outside {
    opacity: .38;
}

.vp-cal__day.has-slots .vp-cal__note,
.vp-cal__day.is-partial .vp-cal__note,
.vp-cal__day.is-full .vp-cal__note {
    color: inherit;
}

.vp-cal__day.is-selected {
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}

/* ---------- auth pages: login, forgot password, change password ----------
   Also rendered inside the mobile app's web view, since tblSetupSite.ForgotPasswordURL points
   here for VP sites - hence the generous tap targets and the 16px inputs, which stop iOS from
   zooming when a field takes focus. */
.vp-auth-page {
    margin: 0;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px 16px;
    background: var(--page-bg);
    font-family: var(--font);
    color: var(--ink);
    -webkit-text-size-adjust: 100%;
}

.vp-card {
    width: 100%;
    max-width: 420px;
    background: var(--card);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}

/* Brand accent carried over from the prototype card headers, reduced to a rule so the
   dark-blue wordmark below it keeps full contrast against white. */
.vp-card__accent {
    height: 4px;
    background: linear-gradient(90deg, var(--brand-ink), var(--grad-a), var(--grad-b));
}

.vp-card__brand {
    padding: 26px 28px 20px;
    text-align: center;
    border-bottom: 1px solid var(--line-soft);
}

.vp-card__brand img {
    display: inline-block;
    width: auto;
    height: 40px;
    max-width: 100%;
}

.vp-card__body {
    padding: 26px 28px 28px;
}

.vp-title {
    margin: 0;
    font-size: 20px;
    font-weight: 700;
    letter-spacing: .2px;
    color: var(--ink-strong);
}

.vp-subtitle {
    margin: 6px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--muted);
}

/* Space between the heading block and whatever follows it, without the heading needing to know
   whether a subtitle was rendered. */
.vp-title + form,
.vp-subtitle + form,
.vp-title + .vp-prose,
.vp-subtitle + .vp-prose {
    margin-top: 22px;
}

@media (max-width: 480px) {
    .vp-card__brand {
        padding: 22px 20px 16px;
    }

    .vp-card__brand img {
        height: 34px;
    }

    .vp-card__body {
        padding: 22px 20px;
    }
}

/* ---------- text ---------- */
.vp-prose {
    margin: 0 0 18px;
    font-size: 14px;
    line-height: 1.6;
}

.vp-prose--muted {
    font-size: 13px;
    color: var(--muted);
}

.vp-hint {
    display: block;
    margin-top: 6px;
    font-size: 12px;
    color: var(--muted);
}

/* ---------- fields ---------- */
.vp-field {
    margin-bottom: 18px;
}

.vp-field label {
    display: block;
    margin-bottom: 6px;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink-strong);
}

.vp-field input {
    width: 100%;
    box-sizing: border-box;
    padding: 12px 13px;
    font-family: inherit;
    /* 16px keeps iOS Safari from zooming the page when the field takes focus. */
    font-size: 16px;
    color: var(--ink-strong);
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 8px;
    transition: border-color .15s, box-shadow .15s;
}

.vp-field input:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px rgba(74, 125, 252, .15);
}

.vp-field input.input-validation-error {
    border-color: var(--danger);
}

/* ---------- buttons and links ---------- */
.vp-btn {
    width: 100%;
    padding: 13px 16px;
    font-family: inherit;
    font-size: 15px;
    font-weight: 600;
    color: #fff;
    background: var(--brand);
    border: 0;
    border-radius: 8px;
    cursor: pointer;
    transition: background .15s, opacity .15s;
}

.vp-btn:hover {
    background: var(--brand-ink);
}

.vp-btn[disabled] {
    opacity: .6;
    cursor: default;
}

.vp-btn--spaced {
    margin-top: 22px;
}

.vp-links {
    margin: 18px 0 0;
    font-size: 13px;
    text-align: center;
}

.vp-links a {
    color: var(--brand);
    text-decoration: none;
}

.vp-links a:hover {
    color: var(--brand-ink);
    text-decoration: underline;
}

/* A form button that should read as a link - used for "Sign out instead", which has to be a
   POST for antiforgery but should not compete with the primary action. */
.vp-linkbtn {
    padding: 0;
    font-family: inherit;
    font-size: 13px;
    color: var(--brand);
    background: none;
    border: 0;
    cursor: pointer;
}

.vp-linkbtn:hover {
    color: var(--brand-ink);
    text-decoration: underline;
}

/* ================= schedule overview =================
   The administrator's day monitor. Bubble colours are the prototype's slot legend - green
   available, pink booked - with an amber middle state the prototype had no need for: its slots
   were one seat each, so a slot was either free or taken. Ours carry a capacity, and a half-full
   slot reading as "available" would overstate what is left. */
.vp-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(124px, 1fr));
    gap: 10px;
}

.vp-stat {
    padding: 12px 14px;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 10px;
}

.vp-stat__num {
    font-size: 22px;
    font-weight: 700;
    line-height: 1.1;
    color: var(--ink-strong);
    font-variant-numeric: tabular-nums;
}

.vp-stat__label {
    margin-top: 2px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
}

.vp-stat--ok .vp-stat__num {
    color: var(--ok-d);
}

.vp-stat--warn .vp-stat__num {
    color: var(--danger);
}

.vp-bubbles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
    gap: 8px;
}

.vp-bubble {
    padding: 7px 10px;
    border: 1px solid;
    border-radius: 10px;
}

.vp-bubble__time {
    display: block;
    font-size: 12.5px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.vp-bubble__seats {
    display: block;
    margin-top: 1px;
    font-size: 11px;
}

.vp-bubble--open {
    background: #e7f8ef;
    border-color: #8fd9b4;
    color: var(--ok-d);
}

.vp-bubble--part {
    background: #fff6e5;
    border-color: #f0cd8c;
    color: #8a5600;
}

.vp-bubble--full {
    background: #fde7ef;
    border-color: #f0a9c6;
    color: #c01860;
}

/* Legend swatches, so the bubble colours are readable without hovering anything. */
.vp-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    font-size: 11.5px;
    color: var(--muted);
}

.vp-legend span {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.vp-legend i {
    display: inline-block;
    width: 12px;
    height: 12px;
    border: 1px solid;
    border-radius: 4px;
}

/* ---------- brush picker (Set Up Schedule) ----------
   The chosen brush has to stay obvious the whole time the officer is clicking days - if it is not,
   a click feels like it did something random. So the selection is a filled card, not a tick.

   The radio itself is visually-hidden rather than display:none: hiding it outright takes it out of
   the tab order and off screen readers, and this IS the control. */
.vp-brushes {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.vp-brush {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 150px;
    padding: 9px 14px;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 10px;
    cursor: pointer;
    transition: border-color .12s, background .12s, box-shadow .12s;
}

.vp-brush:hover {
    border-color: var(--grad-b);
    background: rgba(74, 125, 252, .04);
}

/* Keyboard focus lands on the hidden radio, so the ring has to be drawn on its label. */
.vp-brush:has(:focus-visible) {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.vp-brush__name {
    font-size: 13px;
    font-weight: 700;
    color: var(--ink-strong);
}

.vp-brush__meta {
    font-size: 11.5px;
    color: var(--muted);
}

.vp-brush--on {
    background: #e9efff;
    border-color: var(--brand);
    box-shadow: 0 0 0 1px var(--brand);
}

.vp-brush--on .vp-brush__name {
    color: var(--brand-ink);
}

.vp-brush--off.vp-brush--on {
    background: #fff4e0;
    border-color: var(--warn);
    box-shadow: 0 0 0 1px var(--warn);
}

.vp-brush--clear.vp-brush--on {
    background: #fdeaf1;
    border-color: var(--danger);
    box-shadow: 0 0 0 1px var(--danger);
}

.vp-brush--off.vp-brush--on .vp-brush__name,
.vp-brush--clear.vp-brush--on .vp-brush__name {
    color: var(--ink-strong);
}

/* ---------- paintable calendar ----------
   Day cells are <button> here rather than <a>, because a click writes. Bootstrap and the browser
   both style buttons, so the day styling has to be reasserted. */
.vp-cal--paint .vp-cal__day {
    width: 100%;
    font: inherit;
    appearance: none;
    cursor: pointer;
}

/* A day with bookings will be skipped. Saying so before the click is the point - afterwards it is
   just a number in a summary. Top corner, clear of the meter along the foot. */
.vp-cal--paint .vp-cal__day.is-booked::after {
    content: "";
    position: absolute;
    right: 6px;
    top: 6px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--danger);
}

/* Outside the VP period or in the past nothing can be booked, so nothing should be built. Disabled
   rather than removed, so the month still reads as a month. */
.vp-cal--paint .vp-cal__day:disabled {
    cursor: not-allowed;
}

.vp-legend__booked {
    position: relative;
    background: #fff;
    border-color: var(--line);
}

.vp-legend__booked::after {
    content: "";
    position: absolute;
    right: 1px;
    bottom: 1px;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--danger);
}

/* ---------- sortable column headers ----------
   The header itself is the control, so it inherits the header's weight rather than looking like a
   link dropped into the row - underlining six of seven headers would read as a row of links above
   the data instead of a header.

   The first version showed an arrow on the active column only, and nobody found it: a mark that
   appears after the first click cannot advertise that the click was there. So every sortable header
   carries a faint double arrow at rest, which is the affordance, and the active column is marked
   three ways at once - tinted cell, brand-coloured bold label, and a solid triangle. One of those
   alone was too quiet against a header row that is already bold and dark. */
.vp-sort {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: inherit;
    font: inherit;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
}

.vp-sort:hover,
.vp-sort:focus-visible {
    color: var(--brand-ink);
    text-decoration: none;
}

.vp-sort:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 3px;
    border-radius: 3px;
}

/* Fixed width, so the label does not shift sideways as the sort moves between columns. */
.vp-sort__ind {
    display: inline-block;
    width: 9px;
    font-size: 10px;
    line-height: 1;
    text-align: center;
    color: var(--muted);
    opacity: .75;
    transition: opacity .12s, color .12s;
}

.vp-sort__ind::before {
    content: "\2195";          /* ↕ - sortable, not currently sorted */
}

/* Pointing at the header brightens its own arrow, confirming the header is the control. */
.vp-sort:hover .vp-sort__ind,
.vp-sort:focus-visible .vp-sort__ind {
    color: var(--brand);
    opacity: 1;
}

.vp-sort--asc .vp-sort__ind::before {
    content: "\25B2";          /* ▲ - a filled triangle stays legible at this size; an arrow does not */
}

.vp-sort--desc .vp-sort__ind::before {
    content: "\25BC";          /* ▼ */
}

.vp-sort--asc .vp-sort__ind,
.vp-sort--desc .vp-sort__ind {
    width: auto;
    font-size: 9px;
    color: var(--brand);
    opacity: 1;
}

/* Colour only - a th is already bold, so there is no weight left to add. */
.vp-sort--asc,
.vp-sort--desc {
    color: var(--brand-ink);
}

/* The active column, marked on the cell as well as the label: at a glance it is the tint that says
   which column the list is ordered by, before anyone reads the triangle.

   Bootstrap paints every table cell with "box-shadow: inset 0 0 0 9999px" off its own variables, so
   a plain background on the th is painted straight over. The tint has to go through those variables
   instead - and a flat hex rather than an alpha, since the shadow and the background would otherwise
   stack the same translucent layer twice. The selector is deliberately long enough to outrank
   Bootstrap's own ".table > :not(caption) > * > *".

   The width is set with the colour: inheriting Bootstrap's 1px leaves a hairline that reads as a
   rendering artefact rather than an accent, and at a fractional device pixel ratio it comes out
   thinner still. */
.table > thead > tr > .vp-th--sorted {
    --bs-table-bg: #e9efff;
    --bs-table-accent-bg: #e9efff;
    background-color: #e9efff;
    border-bottom: 2px solid var(--brand);
}

/* ---------- list pager ----------
   Sits inside the card below the table, so the row count and the page links read as belonging to
   that table rather than floating after it. Wraps to two lines on a narrow screen instead of
   pushing the count off the edge. */
.vp-pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 12px 16px;
    border-top: 1px solid var(--line);
}

.vp-pager__count {
    font-size: 12px;
    color: var(--muted);
}

/* ---------- table row actions ----------
   The Edit / View link at the end of a list row. A bare text link disappeared into the row and
   gave no tap target, so it reads as a quiet pill: neutral until pointed at, brand on hover. Not
   a Bootstrap .btn, because .btn-outline-* pulls in Bootstrap's own palette rather than this one. */
.vp-action {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 14px;
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
    color: var(--ink);
    text-decoration: none;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
    transition: color .12s, background .12s, border-color .12s, box-shadow .12s;
}

.vp-action:hover {
    color: var(--brand-ink);
    background: rgba(74, 125, 252, .08);
    border-color: var(--grad-b);
    text-decoration: none;
}

.vp-action:active {
    background: rgba(74, 125, 252, .16);
    border-color: var(--brand);
}

/* One action per line, every button the same width.

   Sized to their own labels and wrapped, these made a different ragged shape in every row - four
   pills of four different widths, breaking to a second line at whatever point the longest unit
   number happened to push them. Down a long list that reads as noise rather than as a column.

   The width is fixed rather than sized to the widest label in each row, so the column is uniform
   from top to bottom: a row offering only "Book" lines up with the row above it offering four.
   It fits "Representative", the longest label, with room to spare. */
.vp-actions-stack {
    display: grid;
    grid-template-columns: 144px;
    gap: 4px;

    /* The stack sits against the right edge of the cell, where the buttons were before. */
    justify-content: end;
}

/* Left-aligned text in a stretched pill would drift away from the button's own centre line. */
.vp-actions-stack .vp-action {
    justify-content: center;
}

/* Keyboard focus has to be visible on its own: the hover tint alone is too weak to locate. */
.vp-action:focus-visible {
    outline: none;
    color: var(--brand-ink);
    border-color: var(--brand);
    box-shadow: 0 0 0 3px rgba(74, 125, 252, .20);
}

/* ---------- alerts ---------- */
.vp-alert {
    margin-bottom: 18px;
    padding: 11px 13px;
    font-size: 13px;
    line-height: 1.45;
    border-radius: 8px;
    background: rgba(216, 27, 96, .08);
    border: 1px solid rgba(216, 27, 96, .25);
    color: var(--danger);
}

/* asp-validation-summary always emits the wrapper, so hide it until it has content. */
.vp-alert:empty,
.vp-alert.validation-summary-valid {
    display: none;
}

.vp-alert ul {
    margin: 0;
    padding-left: 18px;
}

.vp-alert--ok {
    background: rgba(46, 204, 113, .10);
    border-color: rgba(17, 130, 96, .25);
    color: var(--ok-d);
}

/* Amber, for something waiting on a person rather than something wrong: an undecided
   representative request, or an attendee the officer may want to reconsider. The base .vp-alert is
   the danger colour, which overstates both. */
.vp-alert--warn {
    background: rgba(243, 156, 18, .10);
    border-color: rgba(243, 156, 18, .35);
    color: #8a5a06;
}

/* ---------- template block detail (brush bar) ----------
   "4 blocks · 09:00–18:00" does not say whether that is four two-hour appointments or four
   ninety-minute ones around a long lunch. Selecting a template lists the real blocks, because the
   next click applies them across a month. */
.vp-blocks {
    padding: 11px 13px;
    background: #f7f9fc;
    border: 1px solid var(--line);
    border-radius: 10px;
}

.vp-blocks__head {
    margin-bottom: 8px;
    font-size: 12px;
    color: var(--muted);
}

.vp-blocks__list {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.vp-blockchip {
    padding: 3px 9px;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--ink-strong);
    background: #fff;
    border: 1px solid var(--grad-b);
    border-radius: 999px;
    white-space: nowrap;
}

.vp-blockchip b {
    margin-left: 3px;
    color: var(--brand-ink);
}

.vp-blocks__foot {
    margin-top: 8px;
    font-size: 11.5px;
    color: var(--muted);
}

/* ---------- reorder arrows ----------
   Up/down rather than drag and drop: dragging needs a JavaScript library, and this application
   vendors only htmx, Alpine and Bootstrap with no Node toolchain. Arrows also work by keyboard,
   which a drag handle does not. Each is its own form, so they stack tightly. */
.vp-order {
    display: inline-flex;
    flex-direction: column;
    gap: 2px;
}

.vp-order__btn {
    display: block;
    width: 22px;
    padding: 0;
    font-size: 11px;
    line-height: 16px;
    color: var(--ink);
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 5px;
    cursor: pointer;
    transition: border-color .12s, color .12s, background .12s;
}

.vp-order__btn:hover:not(:disabled) {
    color: var(--brand-ink);
    background: rgba(74, 125, 252, .08);
    border-color: var(--grad-b);
}

.vp-order__btn:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}

.vp-order__btn:disabled {
    opacity: .35;
    cursor: default;
}

/* ---------- signature pad ----------
   touch-action: none is what makes this work on a tablet at all. Without it the browser treats a
   drag on the canvas as a scroll and the pointermove events never arrive, so the pad appears dead
   on exactly the device an officer signs a purchaser in on. */
.vp-sign__pad {
    display: block;
    width: 100%;
    height: 140px;
    background: #fff;
    border: 1px dashed var(--grad-b);
    border-radius: 10px;
    touch-action: none;
    cursor: crosshair;
}

.vp-sign__done {
    margin-bottom: 8px;
}

/* The canvas exports its bitmap, which is transparent everywhere the officer did not draw - the
   pad's own white background is CSS and does not travel with the PNG. So the ground has to be
   painted here too, or dark ink on a dark theme would come back invisible. */
.vp-sign__image {
    display: block;
    width: 100%;
    max-width: 320px;
    background: #fff;
    border: 1px solid var(--grad-b);
    border-radius: 10px;
}

/* ---------- unit status list ----------
   A tile that is also a filter. Drawn as an <a> so it inherits nothing from Bootstrap's link
   colour - the number and the label keep their own. */
.vp-stat--link {
    display: block;
    color: inherit;
    text-decoration: none;
    transition: border-color .12s, box-shadow .12s;
}

.vp-stat--link:hover {
    color: inherit;
    border-color: var(--brand);
}

.vp-stat--link.is-active {
    border-color: var(--brand);
    box-shadow: 0 0 0 2px rgba(74, 125, 252, .18);
}

.vp-stat--link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* The stage badge is one fixed width whatever it says, so the dots beside it line up down the
   column. Wide enough for "Documents incomplete" at the badge's own font size. */
/* Seven dots placing a unit on the journey: filled up to and including its stage. Decoration
   under the badge, which carries the words - so it is aria-labelled once, not per dot. */
.vp-steps {
    display: inline-flex;
    gap: 4px;
    align-items: center;
}

.vp-steps__dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--line);
}

.vp-steps__dot.is-done {
    background: var(--brand);
}

/* ---------- dashboard ----------
   Three small data displays, each in plain HTML. Marks are thin, text wears the ink tokens and
   never the data colour, and the data colour is the brand blue throughout - one series, one hue. */

/* The officer's week: one row per day, a meter of booked against capacity. */
.vp-week__row {
    display: grid;
    grid-template-columns: 7.5rem 1fr auto auto;
    gap: 10px;
    align-items: center;
    padding: 7px 0;
    border-top: 1px solid var(--line-soft);
}

.vp-week__row:first-child {
    border-top: 0;
}

.vp-week__row--empty {
    grid-template-columns: 7.5rem 1fr auto;
}

.vp-week__day {
    font-size: 12.5px;
    color: var(--ink);
}

.vp-week__meter {
    display: block;
    height: 6px;
    border-radius: 3px;
    background: var(--line-soft);
    overflow: hidden;
}

.vp-week__fill {
    display: block;
    height: 100%;
    background: var(--brand);
    border-radius: 3px;
}

.vp-week__fill.is-full {
    background: var(--ok-d);
}

.vp-week__count {
    min-width: 6.5rem;
    color: var(--ink-strong);
    font-variant-numeric: tabular-nums;
}

.vp-week__none {
    justify-self: end;
}

/* The funnel: seven stage rows with proportional bars, each label a link to that filter. */
.vp-funnel__row {
    display: grid;
    grid-template-columns: 10.5rem 1fr 2.5rem;
    gap: 10px;
    align-items: center;
    padding: 5px 0;
}

.vp-funnel__label {
    font-size: 12.5px;
    color: var(--ink);
    text-decoration: none;
}

.vp-funnel__label:hover {
    color: var(--brand-ink);
    text-decoration: underline;
}

.vp-funnel__bar {
    display: block;
    height: 10px;
    background: var(--line-soft);
    border-radius: 0 4px 4px 0;
    overflow: hidden;
}

.vp-funnel__fill {
    display: block;
    height: 100%;
    background: var(--brand);
    border-radius: 0 4px 4px 0;
    min-width: 0;
}

.vp-funnel__fill.is-done {
    background: var(--ok-d);
}

.vp-funnel__num {
    text-align: right;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--ink-strong);
    font-variant-numeric: tabular-nums;
}

/* Handovers per week: eight columns on one baseline. Columns are capped at 24px, rounded at the
   data end and square at the baseline; the value shows on the latest and the highest, and on
   hover for the rest. */
.vp-cols {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    gap: 6px;
    height: 132px;
    margin-top: 8px;
    padding-top: 18px;
    border-bottom: 1px solid var(--line);
}

.vp-cols__slot {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    height: 100%;
    min-width: 0;
}

.vp-cols__bar {
    display: block;
    width: 100%;
    max-width: 24px;
    background: var(--grad-b);
    border-radius: 4px 4px 0 0;
    transition: background .12s;
}

.vp-cols__bar.is-current {
    background: var(--brand);
}

.vp-cols__slot:hover .vp-cols__bar {
    background: var(--brand-ink);
}

.vp-cols__val {
    position: absolute;
    bottom: calc(100% - 16px);
    font-size: 11px;
    font-weight: 600;
    color: var(--ink-strong);
    font-variant-numeric: tabular-nums;
    opacity: 0;
    transition: opacity .12s;
}

.vp-cols__val.is-shown,
.vp-cols__slot:hover .vp-cols__val {
    opacity: 1;
}

/* Sits below the baseline: the slot is a column, so a tick after the bar lands under it. */
.vp-cols__tick {
    position: absolute;
    top: 100%;
    margin-top: 4px;
    font-size: 10.5px;
    color: var(--muted);
    white-space: nowrap;
}

.vp-cols + .visually-hidden {
    margin-top: 0;
}

/* Room under the columns for the ticks, so the card padding does not clip them. */
.vp-cols {
    margin-bottom: 22px;
}

/* ---------- confirmation strip ----------
   The step between choosing and doing, for actions a plain form would fire on one click. Bordered
   in the brand colour rather than a warning tone: it is a question, not an error. */
.vp-confirm {
    padding: 12px 14px;
    background: #f4f7ff;
    border: 1px solid var(--grad-b);
    border-left: 3px solid var(--brand);
    border-radius: 10px;
}

.vp-bubble.is-picked {
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}
