﻿/* ==========================================================================
   Staff console. Loaded in addition to app.css, which supplies the tokens.
   ========================================================================== */

/* --------------------------------------------------------------- county map */

/*
   THE RECOLOUR TRICK.

   Every county carries a pre-computed band for all three fill modes
   (data-vband / data-nband / data-zband) and its partisanship for both years
   (data-p24 / data-p20). Which of those the browser paints is decided purely by
   the class on the <svg>.

   So switching fill mode is ONE attribute changing on ONE element. Blazor Server
   sends that as a tiny diff; the alternative — recomputing a fill per county —
   would re-render 159 <path> elements over the wire on every toggle.
*/

.map-wrap {
    position: relative;
}

.county-map {
    display: block;
    width: 100%;
    height: auto;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.county-map path {
    stroke-width: 0.75;
    cursor: pointer;
    transition: fill 0.12s ease;
    /* Default fill, overridden by the mode rules below. A county that somehow
       matches no rule shows as "no data" rather than invisible. */
    fill: #f1ece4;
    stroke: #b9c2cc;
}

.county-map path:hover {
    fill-opacity: 0.82;
}

/*
   A focusable <path> gets the browser's default focus ring, and a browser draws
   that ring around the element's BOUNDING BOX — so a focused county showed a
   black rectangle straddling its neighbours instead of tracing its own border.
   Suppressed on both :focus and :focus-visible (Chrome paints the plain :focus
   ring on SVG), with the focus state carried by the county's own stroke, which
   follows the actual shape.
*/
.county-map path:focus,
.county-map path:focus-visible {
    outline: none;
}

.county-map path:focus-visible {
    stroke: var(--cyan);
    stroke-width: 3;
    /* Paint the focused county's outline over its neighbours' — SVG has no
       z-index, so without this the adjacent shapes overdraw half the ring. */
    paint-order: stroke;
}

/* ---- fill mode: volunteers (green — "how many do we have") ---------------- */
.county-map.mode-volunteers path[data-vband='0'] { fill: #f4f1ec; }
.county-map.mode-volunteers path[data-vband='1'] { fill: #d8ecd6; }
.county-map.mode-volunteers path[data-vband='2'] { fill: #a9d8a4; }
.county-map.mode-volunteers path[data-vband='3'] { fill: #6bbd66; }
.county-map.mode-volunteers path[data-vband='4'] { fill: #2f9e44; }

/* ---- fill mode: organizations (teal — "who is partnered with us") ---------
   Teal rather than another green: organizations sit right beside volunteers in
   the toggle, and two greens would read as the same map with the numbers
   nudged. It has to be obvious at a glance which of the two is on screen. */
.county-map.mode-orgs path[data-oband='0'] { fill: #f4f1ec; }
.county-map.mode-orgs path[data-oband='1'] { fill: #d3ece9; }
.county-map.mode-orgs path[data-oband='2'] { fill: #93d3cc; }
.county-map.mode-orgs path[data-oband='3'] { fill: #43b3a6; }
.county-map.mode-orgs path[data-oband='4'] { fill: #10817a; }

/* ---- fill mode: need (orange — "where the gap is") ------------------------ */
.county-map.mode-need path[data-nband='0'] { fill: #f4f1ec; }
.county-map.mode-need path[data-nband='1'] { fill: #fde3c8; }
.county-map.mode-need path[data-nband='2'] { fill: #fdbb84; }
.county-map.mode-need path[data-nband='3'] { fill: #f57f3d; }
.county-map.mode-need path[data-nband='4'] { fill: #d94801; }

/* ---- fill mode: voters (blue — "where the people are") -------------------- */
.county-map.mode-voters path[data-zband='0'] { fill: #f4f1ec; }
.county-map.mode-voters path[data-zband='1'] { fill: #dce8f5; }
.county-map.mode-voters path[data-zband='2'] { fill: #a8c8e8; }
.county-map.mode-voters path[data-zband='3'] { fill: #5a9bd4; }
.county-map.mode-voters path[data-zband='4'] { fill: #1c6fd6; }

/* ---- partisan outline, per year ------------------------------------------ */
/*
   Outline colour is INDEPENDENT of fill, which is the point of the two toggles:
   a staffer can ask "where are my volunteers" and "which counties are contested"
   at the same time. D blue / R red / C purple, matching the Apps Script build.
*/
.county-map.year-2024 path[data-p24='D'] { stroke: #1c6fd6; }
.county-map.year-2024 path[data-p24='R'] { stroke: #d13b2a; }
.county-map.year-2024 path[data-p24='C'] { stroke: #8e44ad; }

.county-map.year-2020 path[data-p20='D'] { stroke: #1c6fd6; }
.county-map.year-2020 path[data-p20='R'] { stroke: #d13b2a; }
.county-map.year-2020 path[data-p20='C'] { stroke: #8e44ad; }

/*
   Selection outranks partisanship, and has to be declared AFTER it to do so.
   The partisan rules are (0,3,1) — .county-map + .year-* + [data-p*] — so a
   plain `.county-map path.sel` at (0,2,1) silently lost, leaving a selected
   county tinted by its partisanship and distinguishable only by stroke width.
   The [data-fips] here (every path has one) matches that specificity, and
   source order breaks the tie.

   Selection must never be ambiguous: it drives every send and export.
*/
.county-map path.sel[data-fips] {
    stroke: var(--ink);
    stroke-width: 2.5;
}

.county-label {
    font-family: 'Public Sans', sans-serif;
    /* Kept in step with LABEL_FONT_PX in scripts/gen-map-json.js, which decides which labels
       are drawn at all. Change one and the generator is judging a size that is not on screen. */
    font-size: 7.5px;
    font-weight: 600;
    fill: var(--ink);
    text-anchor: middle;
    pointer-events: none;   /* never steal a click from the county underneath */
    paint-order: stroke;
    stroke: rgb(255 255 255 / 75%);
    stroke-width: 2.5px;    /* a halo, so labels stay legible over a dark fill */
}


/* ---- the readout --------------------------------------------------------- */

.county-readout {
    margin: var(--step) 0 0;
    min-height: 1.6em;
    font-size: 0.9375rem;
}

/*
   v1.30.1, carried over. SVG <title> tooltips render on HOVER, which touch
   devices do not have — so on a phone the map could never explain itself. The
   readout is the answer, but inline it lands hundreds of pixels below the county
   being tapped (the map is ~520px tall at 390px wide). On hover-less devices it
   is pinned to the bottom of the viewport instead.
*/
@media (hover: none) {
    .county-readout {
        position: fixed;
        inset: auto 0 0 0;
        z-index: 20;
        margin: 0;
        background: var(--navy-deep);
        color: #fff;
        padding: 0.85rem 1rem;
        box-shadow: var(--shadow-lg);
    }

    .county-readout .hint {
        color: #cfe0ec;
    }
}

/* ---- legend -------------------------------------------------------------- */

.legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem 0.75rem;
    margin-top: var(--step);
    font-size: 0.8125rem;
    color: var(--slate);
}

.legend .swatch {
    display: inline-block;
    width: 1rem;
    height: 0.75rem;
    border-radius: 2px;
    border: 1px solid rgb(16 34 47 / 15%);
    margin-right: 0.3rem;
    vertical-align: -1px;
}

/* --------------------------------------------------------------- console UI */

/* ------------------------------------------------------------- console navigation */

.staff-nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--step) calc(var(--step) * 2);
    max-width: 78rem;
    margin: 0 auto;
    padding: var(--step) calc(var(--step) * 2);
    border-bottom: 1.5px solid var(--line);
}

.staff-nav-brand {
    font-weight: 800;
    letter-spacing: 0.02em;
    color: var(--navy);
    text-decoration: none;
}

.staff-nav ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.staff-nav a:not(.staff-nav-brand) {
    display: block;
    padding: 0.4rem 0.7rem;
    border-radius: var(--radius);
    color: var(--slate);
    text-decoration: none;
    font-size: 0.9375rem;
    /* 44px including padding: these are tapped on a phone at a canvass launch, not only
       clicked at a desk. */
    min-height: 44px;
    display: flex;
    align-items: center;
}

.staff-nav a:not(.staff-nav-brand):hover {
    background: var(--paper);
    color: var(--ink);
}

/* The current page is marked by WEIGHT AND AN UNDERLINE, not colour alone — colour alone is
   the one distinction a red-green colourblind staffer may not see, and this is the only
   indicator of where they are now that each page has stopped announcing itself. */
.staff-nav a.on {
    color: var(--navy);
    font-weight: 700;
    box-shadow: inset 0 -3px 0 var(--cyan);
}

/* Pushed to the far end, away from the destinations — it is information, not navigation. */
.staff-nav-who {
    display: flex;
    align-items: center;
    gap: var(--step);
    margin-left: auto;
    color: var(--slate);
    font-size: 0.8125rem;
}

.staff-nav-who > span {
    /* An address is long and this is the least important thing in the bar, so it gives up its
       width first rather than pushing the destinations off the end. */
    max-width: 16rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.staff-nav-who form {
    margin: 0;
}

.staff-nav-signout {
    background: none;
    border: none;
    padding: 0.25rem 0.4rem;
    color: var(--slate);
    font: inherit;
    font-size: 0.8125rem;
    text-decoration: underline;
    cursor: pointer;
}

.staff-nav-signout:hover {
    color: var(--ink);
}

/* ---- event timeline: notes and recorded actions -------------------------- */

.note-list {
    margin: var(--step) 0 0;
    padding: 0;
    list-style: none;
}

.note-list li {
    padding: var(--step) 0;
    border-top: 1px solid var(--line);
}

.note-list p {
    margin: 0;
}

.note-list p + p {
    margin-top: 0.15rem;
}

/* A recorded action is not something a staffer wrote, and the list must never read as though
   somebody typed "Event cancelled". Recessed and badged, so prose stands out as prose. */
.note-activity {
    color: var(--slate);
}

/* ---- settings landing ---------------------------------------------------- */

.settings-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: calc(var(--step) * 2);
    max-width: 62rem;
}

.settings-card {
    display: block;
    text-decoration: none;
    color: inherit;
}

.settings-card:hover {
    border-color: var(--cyan);
}

.settings-card h2 {
    margin-top: 0;
}

/*
   On a phone, eight wrapped items stacked to three rows and 187px — nearly a quarter of the
   viewport spent on navigation before any content appeared. One scrollable row costs ~60px
   instead.

   The brand goes with it: it is a second link to /staff, which Coverage already is, and on
   375px a redundant link is the first thing that should give up its space.
*/
@media (width <= 700px) {
    .staff-nav {
        flex-wrap: nowrap;
        gap: 0;
    }

    .staff-nav-brand {
        display: none;
    }

    .staff-nav ul {
        flex: 1;
        min-width: 0;
        flex-wrap: nowrap;
        overflow-x: auto;
        /* Snap so a swipe lands on a whole item rather than halfway through a word. */
        scroll-snap-type: x proximity;
        scrollbar-width: none;
    }

    .staff-nav ul::-webkit-scrollbar {
        display: none;
    }

    .staff-nav li {
        scroll-snap-align: start;
    }

    .staff-nav a:not(.staff-nav-brand) {
        white-space: nowrap;
    }
}

.console {
    max-width: 78rem;
    margin: 0 auto;
    padding: calc(var(--step) * 3) calc(var(--step) * 2) calc(var(--step) * 10);
}

.console-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--step);
    margin-bottom: calc(var(--step) * 3);
}

.panel-grid {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    gap: calc(var(--step) * 3);
    align-items: start;
}

@media (max-width: 60rem) {
    .panel-grid {
        grid-template-columns: 1fr;
    }
}

.stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: calc(var(--step) * 2);
    margin-bottom: calc(var(--step) * 3);
}

.stat {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: calc(var(--step) * 2);
}

.stat .value {
    font-family: Fraunces, serif;
    font-variation-settings: 'SOFT' 20, 'WONK' 1;
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
    display: block;
}

.stat .label {
    font-size: 0.8125rem;
    color: var(--slate);
}

/* ---- toggles ------------------------------------------------------------- */

.seg {
    display: inline-flex;
    border-radius: var(--radius);
    overflow: hidden;
    border: 1px solid var(--line-strong);
}

.seg button {
    font: 600 0.8125rem 'Public Sans', sans-serif;
    padding: 0.4rem 0.8rem;
    border: 0;
    border-right: 1px solid var(--line-strong);
    background: var(--card);
    color: var(--navy);
    cursor: pointer;
    min-height: 38px;
}

.seg button:last-child {
    border-right: 0;
}

.seg button.on {
    background: var(--navy);
    color: #fff;
}

.toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--step) calc(var(--step) * 2);
    margin-bottom: calc(var(--step) * 2);
}

.toolbar .label {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--slate);
    margin-right: 0.25rem;
}

@media (pointer: coarse) {
    .seg button {
        min-height: 44px;
        padding-inline: 1rem;
    }
}

/* ---- staff chrome -------------------------------------------------------- */

.staff-bar {
    background: var(--navy-deep);
    color: #fff;
    padding: 0.6rem calc(var(--step) * 2);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--step);
    font-size: 0.875rem;
}

.staff-bar a {
    color: #cfe0ec;
}

.staff-bar .who {
    color: #cfe0ec;
}

.mode-help {
    margin: var(--step) 0 0;
    font-size: 0.875rem;
    color: var(--slate);
    max-width: 60ch;
}

/* --------------------------------------------------------------- access list */

.access-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: calc(var(--step) * 2);
}

.access-table th {
    text-align: left;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--slate);
    padding: 0 0 0.4rem;
    border-bottom: 1px solid var(--line);
}

.access-table td {
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--line);
    overflow-wrap: anywhere;
}

.badge-owner {
    font-size: 0.75rem;
    font-weight: 700;
    background: rgb(37 159 218 / 14%);
    color: var(--navy);
    border-radius: var(--radius-pill);
    padding: 0.1rem 0.6rem;
}

.link-danger {
    all: unset;
    color: var(--danger);
    font-weight: 600;
    font-size: 0.875rem;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
    min-height: 44px;   /* the only destructive control on the page */
    display: inline-flex;
    align-items: center;
}

.add-row {
    display: flex;
    align-items: end;
    gap: var(--step);
    flex-wrap: wrap;
}

.add-row .field {
    flex: 1 1 18rem;
}

.add-row button {
    font: 700 0.9375rem 'Public Sans', sans-serif;
    color: #fff;
    background: var(--navy);
    border: 0;
    border-radius: var(--radius-pill);
    padding: 0.7rem 1.5rem;
    cursor: pointer;
    min-height: 44px;
}

.add-row button:disabled {
    opacity: 0.5;
    cursor: default;
}

/* --------------------------------------------------------------- moderation */

.moderation-card + .moderation-card {
    margin-top: calc(var(--step) * 2);
}

.moderation-head {
    display: flex;
    align-items: start;
    justify-content: space-between;
    gap: var(--step);
}

.moderation-head h2 {
    font-size: 1.25rem;
    margin-bottom: 0.15rem;
}

.badge-blank {
    flex: none;
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    background: #fff4e0;
    color: #a5710a;
    border-radius: var(--radius-pill);
    padding: 0.15rem 0.6rem;
}

.moderation-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--step);
    margin-top: calc(var(--step) * 2);
}

.btn-approve,
.btn-reject,
.btn-quiet {
    font: 700 0.9375rem 'Public Sans', sans-serif;
    border-radius: var(--radius-pill);
    padding: 0.65rem 1.4rem;
    cursor: pointer;
    min-height: 44px;
    border: 1.5px solid transparent;
}

.btn-approve {
    background: var(--success);
    color: #fff;
}

.btn-reject {
    background: var(--danger);
    color: #fff;
}

/* Reject is deliberately the QUIET option until chosen: approving is the common
   outcome, and a destructive action should not be the loudest thing on screen. */
.btn-quiet {
    background: var(--card);
    color: var(--slate);
    border-color: var(--line-strong);
}

.btn-quiet:hover {
    color: var(--ink);
    border-color: var(--slate);
}

.btn-approve:disabled,
.btn-reject:disabled,
.btn-quiet:disabled {
    opacity: 0.55;
    cursor: default;
}

.reject-box {
    margin-top: calc(var(--step) * 2);
    padding: calc(var(--step) * 2);
    background: var(--danger-bg);
    border: 1.5px solid rgb(179 38 30 / 25%);
    border-radius: var(--radius);
}

/* ------------------------------------------------- queue filter & bulk actions */

.queue-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: calc(var(--step) * 2);
    max-width: 52rem;
    margin-bottom: calc(var(--step) * 2);
}

.field--inline {
    margin: 0;
    min-width: 14rem;
}

.queue-bar-actions {
    display: flex;
    gap: var(--step);
    margin-left: auto;
}

.queue-bar-actions .btn-quiet {
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    min-height: 40px;
}

/* The reason field needs a doubled class to outweigh app.css's shared field rule, which
   is input + six :not() attribute selectors = (0,6,1). A single class + attribute is
   (0,2,1) and loses, so border/padding/font were silently dead. */

/* Sticky so a long queue never strands the actions off-screen: the whole point of
   selecting 40 events is not having to scroll back to a button. */
.queue-bulk {
    position: sticky;
    top: var(--step);
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--step);
    max-width: 52rem;
    margin-bottom: calc(var(--step) * 2);
    padding: var(--step) calc(var(--step) * 2);
    background: var(--navy);
    color: #fff;
    border-radius: var(--radius-pill);
    box-shadow: 0 6px 20px rgb(17 55 86 / 25%);
}

.queue-bulk .bulk-reason.bulk-reason {
    flex: 1 1 12rem;
    min-height: 40px;
    padding: 0.4rem 0.9rem;
    border: none;
    border-radius: var(--radius-pill);
    font: 400 0.9375rem 'Public Sans', sans-serif;
}

.queue-bulk .btn-approve,
.queue-bulk .btn-reject {
    padding: 0.5rem 1.2rem;
    font-size: 0.875rem;
    min-height: 40px;
}

.pick {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: var(--step);
    font-size: 0.8125rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--slate);
    cursor: pointer;
}

.pick input {
    width: 1.15rem;
    height: 1.15rem;
    accent-color: var(--cyan);
    cursor: pointer;
}

.moderation-card.picked {
    border-color: var(--cyan);
    box-shadow: 0 0 0 3px rgb(37 159 218 / 18%);
}

@media (width <= 640px) {
    .queue-bar-actions {
        margin-left: 0;
    }

    .queue-bulk {
        border-radius: var(--radius);
    }
}

.console-head-actions {
    display: flex;
    align-items: center;
    gap: calc(var(--step) * 2);
}

/* ------------------------------------------------------------- add event form */

/* An <a> styled as a button needs the text centred on the same line box a <button>
   gives it for free. */
.btn-link {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
}

.check {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    cursor: pointer;
    font-weight: 600;
}

.check input {
    width: 1.15rem;
    height: 1.15rem;
    accent-color: var(--cyan);
    cursor: pointer;
}

.rule {
    border: none;
    border-top: 1.5px solid var(--line);
    margin: 0;
}

/* The recurrence preview is the safety net for the one control on this page whose
   effect is not visible from its inputs. Styled as a quiet aside, not an alert:
   it confirms, it does not warn. */
.preview {
    background: var(--paper);
    border: 1.5px solid var(--line);
    border-left: 4px solid var(--cyan);
    border-radius: var(--radius);
    padding: calc(var(--step) * 2);
}

.preview p {
    margin: 0 0 var(--step);
}

.preview ul {
    margin: 0;
    padding-left: 1.2rem;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    font-size: 0.9375rem;
}

.preview li.hint {
    list-style: none;
    margin-left: -1.2rem;
    padding-top: 0.2rem;
}

/* ------------------------------------------------------------------ data tables */

/* The wrapper scrolls, not the page: a table with seven columns will not fit a phone,
   and a horizontally scrolling BODY breaks every other layout on the page. */
.table-wrap {
    max-width: 78rem;
    overflow-x: auto;
    border: 1.5px solid var(--line);
    border-radius: var(--radius);
    background: var(--card);
}

.hub-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9375rem;
}

.hub-table th {
    text-align: left;
    font-size: 0.6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--slate);
    padding: 0.75rem 1rem;
    border-bottom: 1.5px solid var(--line);
    white-space: nowrap;
}

.hub-table td {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--line);
    vertical-align: top;
}

.hub-table tr:last-child td {
    border-bottom: none;
}

.hub-table .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.hub-table .actions {
    white-space: nowrap;
}

.hub-table .actions button {
    padding: 0.35rem 0.8rem;
    font-size: 0.8125rem;
    min-height: 36px;
}

.hub-table tr.unplaced {
    background: #fffaf0;
}

/* ---- selection ring & sign-hub pins -------------------------------------- */

/*
   The selected county's outline, drawn as a stroke-only clone AFTER every county and
   label. SVG has no z-index and the paths are emitted alphabetically — an order with no
   relation to adjacency — so a neighbour drawn later painted its fill over the shared
   border and erased that segment of the ring. Which segment survived was alphabetical
   luck. Re-drawing on top is the whole fix; `paint-order` never helped, because it
   reorders fill and stroke WITHIN one element, not between elements.
*/
.county-map path.sel-ring {
    fill: none;
    stroke: var(--ink);
    stroke-width: 2.5;
    pointer-events: none;   /* must not steal the click from the county underneath */
}

.hub-pin {
    fill: var(--gold);
    stroke: var(--navy-deep);
    /* Thinner ring to match the smaller radius. A 1.25 stroke on a 3px circle is a third of
       its width, which reads as a blob rather than a pin. */
    stroke-width: 0.9;
    pointer-events: none;   /* a pin sits on top of a county; the county owns the click */
}

/* ---------------------------------------------------------------- slide-in drawer */

/*
   The scrim is always in the DOM and toggles opacity rather than display, so the panel can
   actually animate in. visibility rides along so it cannot be tabbed into while closed —
   an off-screen panel that still takes focus is a keyboard trap.
*/
.drawer-scrim {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgb(10 36 56 / 45%);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s;
}

.drawer-scrim.open {
    opacity: 1;
    visibility: visible;
}

.drawer {
    position: fixed;
    inset: 0 0 0 auto;
    z-index: 50;
    width: min(30rem, 100vw);
    padding: calc(var(--step) * 3);
    background: var(--card);
    border-left: 1.5px solid var(--line);
    box-shadow: var(--shadow-lg);
    overflow-y: auto;
    transform: translateX(100%);
    visibility: hidden;
    transition: transform 0.22s cubic-bezier(0.32, 0.72, 0, 1), visibility 0.22s;
}

.drawer.open {
    transform: translateX(0);
    visibility: visible;
}

.drawer-head {
    display: flex;
    align-items: start;
    justify-content: space-between;
    gap: var(--step);
    margin-bottom: calc(var(--step) * 2);
}

.drawer-head h2 {
    margin: 0;
}

.drawer-close {
    flex: none;
    background: none;
    border: none;
    font-size: 1.75rem;
    line-height: 1;
    color: var(--slate);
    cursor: pointer;
    padding: 0 0.25rem;
    min-height: 44px;
    min-width: 44px;
}

.drawer-close:hover {
    color: var(--ink);
}

/* Wider variant, for a panel that carries a form AND the record it is about. */
.drawer--wide {
    width: min(38rem, 100vw);
}

.drawer-section {
    margin: calc(var(--step) * 4) 0 var(--step);
    padding-top: calc(var(--step) * 2);
    border-top: 1.5px solid var(--line);
    font-size: 1.05rem;
}

.drawer-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--step);
    margin-top: calc(var(--step) * 2);
}

.detail-list {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--step) calc(var(--step) * 2);
    margin: 0;
}

.detail-list dt {
    color: var(--slate);
    font-size: 0.8125rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding-top: 0.15rem;
}

.detail-list dd {
    margin: 0;
}

/* Respect a reduced-motion preference: the panel still appears, it just does not slide. */
@media (prefers-reduced-motion: reduce) {
    .drawer,
    .drawer-scrim {
        transition: none;
    }
}

/* ------------------------------------------------------------- ten-day calendar */

.daystrip {
    display: grid;
    /* Column count is the caller's decision, passed as a custom property: five across on the
       events page, one down in the console sidebar. A media query cannot express that,
       because the two live at the same viewport width. */
    grid-template-columns: repeat(var(--daystrip-cols, 5), minmax(0, 1fr));
    gap: var(--step);
    margin-top: calc(var(--step) * 2);
    align-items: start;
}

.daystrip-day {
    display: flex;
    flex-direction: column;
    gap: var(--step);
    min-width: 0;
    padding: var(--step);
    background: var(--card);
    border: 1.5px solid var(--line);
    border-radius: var(--radius);
}

/* A day with nothing on it recedes rather than disappearing — the gap in the week is
   itself information, and a missing column would make the strip hard to read against
   a calendar. */
.daystrip-day.empty {
    background: transparent;
    border-style: dashed;
}

.daystrip-day.today {
    border-color: var(--cyan);
    box-shadow: 0 0 0 1.5px var(--cyan);
}

.daystrip-day > header {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem;
    padding-bottom: var(--step);
    border-bottom: 1px solid var(--line);
}

.daystrip-day .dow {
    color: var(--slate);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.daystrip-day .dom {
    font-weight: 700;
    font-size: 1.1rem;
}

.daystrip-day .mon {
    color: var(--slate);
    font-size: 0.75rem;
}

.badge-today {
    flex-basis: 100%;
    color: var(--cyan-deep);
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.daystrip-event {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    width: 100%;
    padding: var(--step);
    text-align: left;
    font: inherit;
    cursor: pointer;
    background: var(--paper);
    border: 1px solid var(--line);
    /* The turnout colour is a left edge rather than a fill: at this column width a filled
       card fights the day's own border, and the edge still reads down a whole column. */
    border-left-width: 4px;
    border-radius: var(--radius);
}

.daystrip-event:hover,
.daystrip-event:focus-visible {
    border-color: var(--cyan);
    background: var(--card);
}

.daystrip-event .time {
    font-weight: 700;
    font-size: 0.8125rem;
}

.daystrip-event .title {
    /* Two lines, then ellipsis. Ten columns on a laptop is ~9rem each, and an untruncated
       title made every column as tall as the longest event name in the fortnight. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 0.875rem;
    line-height: 1.25;
}

.daystrip-event .where,
.daystrip-event .counts {
    color: var(--slate);
    font-size: 0.6875rem;
}

/* Red is not decoration here — an event with nobody coming is the one thing on this grid
   worth acting on today, so it gets the only alarming colour on the page. */
.daystrip-event.turnout-good { border-left-color: #2f9e44; }
.daystrip-event.turnout-some { border-left-color: var(--gold); }
.daystrip-event.turnout-none { border-left-color: var(--danger); }

/* ---- the console's five-day version -------------------------------------- */

.daystrip--compact {
    margin-top: var(--step);
}

/* Grid, not a flex row. As a row the day's events laid out horizontally, so a Saturday with
   six of them ran straight off the side of the viewport. Here the date is a fixed first
   column and every event stacks under the last in the second. */
.daystrip--compact .daystrip-day {
    display: grid;
    grid-template-columns: 4rem minmax(0, 1fr);
    align-items: start;
    gap: 0.25rem var(--step);
    padding: calc(var(--step) * 0.75) var(--step);
}

.daystrip--compact .daystrip-day > header {
    grid-column: 1;
    padding-bottom: 0;
    border-bottom: none;
}

.daystrip--compact .daystrip-event,
.daystrip--compact .daystrip-day > p {
    grid-column: 2;
    padding: 0.25rem var(--step);
}

/* The county and the counts sit on one line here, because a sidebar row has the width for it
   and four stacked lines per event would make five days taller than the map beside it. */
.daystrip--compact .daystrip-event {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--step);
    align-items: baseline;
}

.daystrip--compact .daystrip-event .title {
    -webkit-line-clamp: 1;
}

.daystrip--compact .daystrip-event .where,
.daystrip--compact .daystrip-event .counts {
    grid-column: 2;
}

/* An empty day is worth a row in the events grid — the gap in the month is information —
   but not in a five-row sidebar, where it is three quarters of the space. */
.daystrip--compact .daystrip-day.empty {
    display: none;
}

/* Five columns is already the narrow layout. Below tablet the grid collapses to one, which
   is what a phone wants anyway. */
@media (width <= 900px) {
    .daystrip {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (width <= 600px) {
    .daystrip {
        grid-template-columns: 1fr;
    }

    /* An empty day in a vertical list is a wasted row, not an informative gap. */
    .daystrip-day.empty {
        display: none;
    }
}

/* ---- calendar block chrome ------------------------------------------------ */

.calendar-block {
    margin-bottom: calc(var(--step) * 5);
}

.cal-nav {
    display: flex;
    align-items: center;
    gap: var(--step);
}

.cal-range {
    font-weight: 600;
    /* Fixed width so stepping through windows does not jog the arrows sideways — the two
       buttons either side of it are the ones being clicked repeatedly. */
    min-width: 12rem;
    text-align: center;
}

.section-head {
    margin: calc(var(--step) * 4) 0 calc(var(--step) * 2);
    padding-top: calc(var(--step) * 2);
    border-top: 1.5px solid var(--line);
}

/* --------------------------------------------------------------- column mapping */

.map-columns {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: calc(var(--step) * 2);
    margin-bottom: calc(var(--step) * 2);
}

/* The heading being mapped, not a form label: it is data from the pasted file, so it can be
   long and arbitrary and must not stretch the grid. */
.map-columns .field > span:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ------------------------------------------------------------------ documentation */

/* Prose, not a dashboard: a readable measure matters more than filling the width. */
.doc {
    max-width: 52rem;
}

.doc .lede {
    max-width: 46ch;
}

.doc .steps {
    list-style: none;
    counter-reset: step;
    margin: 0;
    padding: 0;
}

.doc .steps > li {
    counter-increment: step;
    position: relative;
    padding: 0 0 calc(var(--step) * 4) calc(var(--step) * 6);
    border-left: 2px solid var(--line);
    margin-left: 1rem;
}

.doc .steps > li:last-child {
    border-left-color: transparent;
    padding-bottom: 0;
}

/* The number sits ON the rule, so the steps read as one connected sequence. */
.doc .steps > li::before {
    content: counter(step);
    position: absolute;
    left: -1.1rem;
    top: 0;
    width: 2.2rem;
    height: 2.2rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--navy);
    color: #fff;
    border-radius: 50%;
    font: 700 0.9375rem 'Public Sans', sans-serif;
}

.doc .steps h2 {
    font-size: 1.375rem;
    margin: 0.2rem 0 var(--step);
}

.doc ul {
    max-width: var(--measure);
}

.doc code {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 4px;
    padding: 0.1rem 0.35rem;
    font-size: 0.9em;
}

/* ------------------------------------------------------------------- staff reports */

/*
    Tables, not charts. Every one of these answers "which row should I act on", and a row is
    something you can read, sort and copy into a message — a sparkline is not.
*/
.report-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: calc(var(--step) * 2);
    font-size: 0.9375rem;
}

.report-table th,
.report-table td {
    padding: 0.45rem 0.6rem;
    border-bottom: 1px solid var(--line);
    text-align: left;
}

.report-table th {
    font-size: 0.8125rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--slate);
}

.report-table .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Only the cells that mean "do something" are coloured. Colouring every number would make
   the page decorative and the signal unfindable. */
.cell-warn { color: #b3401c; font-weight: 700; }
.cell-good { color: #1d7a4c; font-weight: 700; }

.funnel {
    list-style: none;
    margin: calc(var(--step) * 2) 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--step) * 2);
}

.funnel li {
    flex: 1 1 8rem;
    /* minmax-free equivalent for flex: a long label must not set the floor width. */
    min-width: 0;
    padding: calc(var(--step) * 1.5);
    border: 1.5px solid var(--line);
    border-radius: var(--radius);
    background: var(--paper);
}

.funnel-value { display: block; font-size: 1.5rem; font-weight: 700; color: var(--navy); }
.funnel-label { display: block; font-size: 0.875rem; }
.funnel-note { display: block; font-size: 0.8125rem; color: var(--slate); margin-top: 0.15rem; }
