/*
 | portalworks Fleet — application stylesheet.
 |
 | Hand-written and served straight from public/. There is no npm build and no
 | bundler in this project on purpose (ADR 1, ADR 13): a new developer clones the
 | repo and gets a working UI without a Node toolchain.
 |
 | Load order matters and is set in resources/views/layouts/app.blade.php:
 |   1. vendor/bootstrap/bootstrap.min.css   the grid, the reset, the components
 |   2. css/fonts.css                        Inter, self-hosted
 |   3. css/brand.css                        portalworks tokens + Bootstrap overrides
 |   4. css/app.css                          this file: layout of our own screens
 |
 | Every colour here comes from a --sn-* token. A literal hex in this file is a
 | brand value that escaped brand.css, and belongs back there.
 */

/* ============================================================= auth screens */

.auth {
    min-height: 100vh;
    display: grid;
    grid-template-columns: 1fr;
    background: var(--sn-bg);
}

/* From lg up the screen splits: brand on the left, the form on the right. Below
   that the brand panel becomes a slim header so the form is never pushed off a
   phone screen. */
@media (min-width: 992px) {
    .auth {
        grid-template-columns: 5fr 7fr;
    }
}

.auth-aside {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 2rem;
    padding: 1.6rem 1.5rem;
    background: var(--sn-gradient-deep);
    color: var(--sn-on-blue);
}

@media (min-width: 992px) {
    .auth-aside {
        padding: 3rem 3rem 2.6rem;
    }
}

/* A soft light source in the corner, so the panel is not a flat block of blue. */
.auth-aside::after {
    content: "";
    position: absolute;
    inset: auto -30% -45% auto;
    width: 26rem;
    height: 26rem;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.22), transparent 65%);
    pointer-events: none;
}

.auth-aside > * {
    position: relative;
    z-index: 1;
}

.auth-claim {
    max-width: 26ch;
    margin: 0;
    font-size: clamp(1.35rem, 2.6vw, 2rem);
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    color: var(--sn-on-blue);
}

.auth-claim-sub {
    margin: 0.9rem 0 0;
    max-width: 42ch;
    font-size: 0.92rem;
    color: rgba(255, 255, 255, 0.85);
}

/* The verified fleet numbers. They belong on this screen: they are what the
   application is for, and they are the first thing that makes it credible. */
.auth-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 1.4rem 2.2rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.auth-facts .v {
    font-size: 1.32rem;
    font-weight: 700;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--sn-on-blue);
}

.auth-facts .k {
    margin-top: 0.15rem;
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.72);
}

.auth-claim,
.auth-facts {
    display: none;
}

@media (min-width: 992px) {
    .auth-claim,
    .auth-facts {
        display: block;
    }

    .auth-facts {
        display: flex;
    }
}

.auth-main {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2.2rem 1.25rem 3rem;
}

@media (min-width: 992px) {
    .auth-main {
        padding: 3rem;
    }
}

.auth-form {
    width: 100%;
    max-width: 26rem;
}

.auth-form h1 {
    margin: 0 0 0.4rem;
}

.auth-lede {
    margin: 0 0 1.75rem;
    font-size: 0.9rem;
    color: var(--sn-text);
}

.auth-alt {
    margin: 1.5rem 0 0;
    text-align: center;
    font-size: 0.88rem;
    color: var(--sn-text);
}

.auth-foot {
    margin: 2.5rem 0 0;
    font-size: 0.74rem;
    color: var(--sn-faint);
    text-align: center;
}

/* ------------------------------------------------- two-factor enrolment */

/* The enrolment screen carries a QR code and three steps, so it needs more room than a
   sign-in form. Still capped: a measure that runs the width of a monitor is unreadable. */
.auth-form-wide {
    max-width: 32rem;
}

.setup-steps {
    margin: 0 0 1.5rem;
    padding-left: 1.2rem;
    font-size: 0.9rem;
    color: var(--sn-text);
}

.setup-steps > li {
    margin-bottom: 1.1rem;
}

/* White always, and not because of the theme: a QR code is read by a camera, and an
   inverted one is not reliably scannable. The padding is the quiet zone the format
   requires. */
/* Its own line. As an inline-block it shared a line box with the step's text, which
   dragged the list marker and the sentence to the vertical middle of a 220px image. */
.qr {
    display: block;
    width: fit-content;
    margin: 0.6rem 0 0.2rem;
    padding: 0.6rem;
    border-radius: var(--sn-radius-sm);
    background: #ffffff;
    line-height: 0;
}

.sn-secret {
    display: inline-block;
    padding: 0.15rem 0.4rem;
    border-radius: 6px;
    background: var(--sn-panel-2);
    font-family: var(--sn-mono);
    font-size: 0.82rem;
    color: var(--sn-title);
    user-select: all;
    overflow-wrap: anywhere;
}

/* Wide, spaced digits: this field is typed from a phone screen under time pressure. */
.sn-code {
    font-family: var(--sn-mono);
    font-size: 1.15rem;
    letter-spacing: 0.28em;
}

.recovery-codes {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: 0.5rem;
    margin: 0 0 1rem;
    padding: 1rem;
    list-style: none;
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius-sm);
    background: var(--sn-panel-2);
}

.recovery-codes > li {
    font-size: 0.86rem;
    color: var(--sn-title);
    user-select: all;
}

/* A submit button that has to look like a link — the sign-out on the enrolment screen
   posts a form, and a link cannot POST. */
.link-button {
    padding: 0;
    border: 0;
    background: none;
    color: var(--bs-link-color);
    text-decoration: underline;
    font: inherit;
    cursor: pointer;
}

/* ============================================================== application */

.app-topbar {
    background: var(--sn-panel);
    border-bottom: 1px solid var(--sn-line);
}

.app-nav {
    display: flex;
    align-items: center;
    gap: 1.4rem;
}

.app-nav a {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.35rem 0;
    font-size: 0.88rem;
    font-weight: 500;
    color: var(--sn-text);
    text-decoration: none;
    border-bottom: 2px solid transparent;
}

.app-nav a:hover {
    color: var(--sn-title);
}

.app-nav a.on {
    color: var(--sn-title);
    border-bottom-color: var(--sn-blue);
}

/* .navbar-toggler/.navbar-toggler-icon are Bootstrap classes, but their colours
   come from --bs-navbar-* custom properties Bootstrap only ever defines inside
   a real .navbar element (bootstrap.min.css) — this app's toggler has no .navbar
   ancestor (adding one would also bring .navbar's own flex/justify-content
   layout, which .app-topbar-brand doesn't want), so the two values actually
   used are supplied here instead, copied from Bootstrap's own light/dark
   defaults. Same reuse-the-classes-not-the-cascade precedent as modal.js. */
.navbar-toggler {
    --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);
}

.navbar-toggler-icon {
    --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}

[data-bs-theme="dark"] .navbar-toggler-icon {
    --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}

/* Below lg (991.98px, Bootstrap's own breakpoint): .app-nav starts collapsed —
   the same reason a plain d-flex row with no wrap overflowed the viewport on
   every screen at mobile width (ADR 91). Shown as a full-width, stacked panel
   once public/js/nav.js adds .show, toggled by the .navbar-toggler button. */
@media (max-width: 991.98px) {
    /* .app-nav's own parent doesn't wrap by default — it holds only the logo
       and the toggler at this width otherwise, laid out inline. Wrapping it
       is what lets the nav (width: 100%) drop to its own line below them,
       rather than squeezing sideways into a flex row with no room left. */
    .app-topbar-brand {
        flex-wrap: wrap;
    }

    .app-nav {
        display: none;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        width: 100%;
        order: 3;
    }

    .app-nav.show {
        display: flex;
    }

    .app-nav a {
        padding: 0.6rem 0;
        border-bottom: 1px solid var(--sn-line);
    }

    .app-nav a.on {
        border-bottom-color: var(--sn-line);
    }

    .navbar-toggler {
        border-color: var(--sn-line);
    }
}

.app-count {
    min-width: 1.2rem;
    padding: 0 0.35rem;
    border-radius: var(--sn-radius-pill);
    background: var(--sn-gradient);
    color: var(--sn-on-blue);
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1.15rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.app-main {
    padding-top: 2rem;
    padding-bottom: 4rem;
}

.page-head h1 {
    margin: 0 0 0.4rem;
}

.page-head p {
    margin: 0;
    max-width: 68ch;
    font-size: 0.92rem;
    color: var(--sn-text);
}

/* ------------------------------------------------------------------- tiles */

.tile {
    height: 100%;
    padding: 1.1rem 1.2rem;
    background: var(--sn-panel);
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius);
    text-decoration: none;
    color: inherit;
    display: block;
    transition: border-color 0.18s ease, box-shadow 0.18s ease;
}

a.tile:hover {
    border-color: var(--sn-blue);
    box-shadow: var(--sn-shadow-sm);
}

.tile .k {
    font-size: 0.66rem;
    font-weight: 600;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--sn-text);
}

.tile .v {
    margin-top: 0.35rem;
    font-size: 1.7rem;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
    color: var(--sn-title);
}

.tile .n {
    margin-top: 0.1rem;
    font-size: 0.78rem;
    color: var(--sn-faint);
}

.tile.is-waiting .v {
    color: var(--sn-ghost);
}

.tile.is-live {
    border-color: var(--sn-blue);
    background: var(--sn-blue-tint);
}

.tile.is-live .v {
    color: var(--sn-blue-deep);
}

/*
 * A tile whose number is bad news.
 *
 * `is-live` paints a figure green, which is right for "this part is working" and wrong
 * for "328 sites are a major version behind" — green reads as good, and status colour is
 * not brand colour (CLAUDE.md). A count that wants attention gets the critical tone
 * instead, and the label above it still says what it counts.
 */
.tile.is-alarm {
    border-color: var(--sn-crit-line);
    background: var(--sn-crit-tint);
}

.tile.is-alarm .v {
    color: var(--sn-crit);
}

/* ------------------------------------------------------------------ panels */

.panel {
    padding: 1.35rem 1.4rem;
    background: var(--sn-panel);
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius);
}

.panel > h2 {
    margin: 0 0 1rem;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--sn-text);
}

.panel-empty {
    margin: 0;
    font-size: 0.9rem;
    color: var(--sn-faint);
}

/* ------------------------------------------------------------------- steps */

.steps {
    margin: 0;
    padding: 0;
    list-style: none;
}

.steps li {
    display: grid;
    grid-template-columns: 5.2rem 1fr;
    gap: 0.9rem;
    align-items: baseline;
    padding: 0.7rem 0;
    border-bottom: 1px solid var(--sn-line-soft);
    font-size: 0.9rem;
    color: var(--sn-text);
}

.steps li:first-child {
    padding-top: 0;
}

.steps li:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.steps b {
    color: var(--sn-title);
    font-weight: 600;
}

.steps .state {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--sn-faint);
}

.steps .state.done {
    color: var(--sn-ok);
}

.steps .state.next {
    color: var(--sn-blue-deep);
}

@media (max-width: 575.98px) {
    .steps li {
        grid-template-columns: 1fr;
        gap: 0.15rem;
    }
}

/* --------------------------------------------------------- site detail screen */

.page-back {
    margin: 0 0 0.35rem;
    font-size: 0.82rem;
}

.page-back a {
    color: var(--sn-text);
    text-decoration: none;
}

.page-back a:hover {
    color: var(--sn-blue-fg);
}

/* The sentence that says what "Behind by" does and does not mean. It is small, and it
   is on the screen rather than only in the documentation, because the column would
   otherwise be read as a vulnerability severity. */
.page-note {
    margin: 0.6rem 0 0;
    max-width: 78ch;
    font-size: 0.84rem;
    color: var(--sn-faint);
}

/* Not an error panel. Nothing failed — the write path does not exist yet. */
.panel-notice {
    border-color: var(--sn-warn-line);
    background: var(--sn-warn-tint);
}

.panel-notice > h2 {
    color: var(--sn-warn);
}

/* The command that would run, and the reason a read failed. Both are output from
   somewhere else, so they are shown verbatim and allowed to wrap. */
.sn-plan {
    margin: 0;
    padding: 0.7rem 0.85rem;
    border: 1px solid var(--sn-line);
    border-radius: var(--sn-radius-sm);
    background: var(--sn-panel-2);
    font-family: var(--sn-mono);
    font-size: 0.78rem;
    color: var(--sn-title);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.sn-faint {
    color: var(--sn-faint);
}

/* A one-line caution under a control. A class rather than a style attribute: the CSP is
   default-src 'self' with no unsafe-inline, so an inline style is blocked outright — the
   first version of this used one and would have rendered unstyled. */
.sn-hint {
    margin-top: 0.25rem;
    font-size: 0.72rem;
    color: var(--sn-warn);
}

/* ------------------------------------------------------------------ tables */

.table {
    --bs-table-color: var(--sn-title);
    --bs-table-border-color: var(--sn-line-soft);
    margin-bottom: 0;
    font-size: 0.88rem;
}

.table > thead th {
    font-size: 0.66rem;
    font-weight: 600;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    color: var(--sn-text);
    white-space: nowrap;
    border-bottom-color: var(--sn-line);
}

.table > tbody td {
    vertical-align: middle;
    color: var(--sn-text);
}

.table > tbody td.strong {
    color: var(--sn-title);
    font-weight: 500;
}

/* The domain is the way into a site. A link rather than a whole clickable row: a row
   that navigates on click takes the text selection with it, and these values get
   copied into tickets. */
.row-link {
    color: var(--sn-title);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.row-link:hover {
    color: var(--sn-blue-fg);
    border-bottom-color: var(--sn-blue-fg);
}

.decide {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: flex-start;
}

@media (min-width: 992px) {
    .decide {
        justify-content: flex-end;
    }
}

/* The rejection reason sits inline next to two buttons in a table row, so it is
   deliberately shorter than a field on a form. It has to opt out of the shared
   --sn-field-h height, or it grows to match the /sites filter. */
.decide .form-control {
    min-height: 0;
    padding: 0.45rem 0.75rem;
    font-size: 0.82rem;
    max-width: 11rem;
}

/* -------------------------------------------------- theme switch placement */

/* In the blue panel the switch sits with the logo row on a phone, and drops to
   its own line from lg up where the panel is tall. */
.auth-aside-foot {
    order: -1;
    display: flex;
    justify-content: flex-end;
}

@media (min-width: 992px) {
    .auth-aside-foot {
        order: 0;
        justify-content: flex-start;
    }
}

/* -------------------------------------------------------------------- modal */

/* Bootstrap's own CSS already class-drives .modal-backdrop's fade-in (see
   bootstrap.min.css). The one rule it deliberately leaves to its JS — toggling
   .modal between display:none and display:block via an inline style — is added
   here instead, because this application's CSP forbids inline styles. See
   public/js/modal.js for why there is no Bootstrap JS bundle loaded at all. */
.modal.show {
    display: block;
}

body.modal-open {
    overflow: hidden;
}

/* --------------------------------------------------------------- busy lock */

/* The operator's own ask: clicking Update/Backup/Restore should not leave
   every other button on the screen still clickable while it runs — a second
   click before the first finishes is exactly how a real problem happens.
   public/js/busy-lock.js locks every button/link the moment any form
   submits and shows this bar; the page's own reload once the server
   responds is what clears both, so there is no JS "unlock" to write. No
   real percentage exists to show — every one of these actions is a single
   synchronous request — so the bar is honestly indeterminate, not a number
   this application cannot measure. */
.busy-bar {
    position: fixed;
    inset: 0 0 auto 0;
    height: 3px;
    z-index: 2000;
    overflow: hidden;
    background: var(--sn-line);
}

.busy-bar[hidden] {
    display: none;
}

.busy-bar::after {
    content: "";
    position: absolute;
    inset: 0;
    width: 40%;
    background: var(--sn-gradient);
    animation: sn-busy-sweep 1.1s ease-in-out infinite;
}

@keyframes sn-busy-sweep {
    0% {
        transform: translateX(-100%);
    }

    100% {
        transform: translateX(350%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .busy-bar::after {
        animation: none;
        width: 100%;
        transform: none;
    }
}

button:disabled,
a.btn.disabled {
    cursor: not-allowed;
    opacity: 0.65;
    pointer-events: none;
}

/* ---------------------------------------------------------------- busy modal */

/* ADR 97, the operator's own direct follow-up to the bar above: a small
   modal that cannot be closed around, so nothing else on the page is
   reachable while an action (Update/Backup/Restore) is in flight — the top
   bar alone was easy to miss, and every button underneath it still looked
   clickable even though app.css already disabled them. Reuses the sweep
   animation .busy-bar already defined, at modal scale rather than a hairline. */
.busy-bar-inline {
    position: relative;
    height: 6px;
    border-radius: var(--sn-radius-pill);
    overflow: hidden;
    background: var(--sn-line);
}

.busy-bar-inline::after {
    content: "";
    position: absolute;
    inset: 0;
    width: 40%;
    background: var(--sn-gradient);
    animation: sn-busy-sweep 1.1s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .busy-bar-inline::after {
        animation: none;
        width: 100%;
        transform: none;
    }
}

/* ------------------------------------------------------------- busy toast */

/* Download's own indicator (ADR 97) — not the modal above. A file download
   never navigates the current page away, so there is no "the server
   responded" moment to hook the way a form's own redirect gives every other
   action; a modal with no way to know when to close would stay stuck open
   forever. This is a plain, honest compromise instead: a small corner note
   that names itself and clears on its own after a few seconds, never
   blocking the rest of the page the way the modal does. */
.busy-toast {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 2000;
    max-width: 20rem;
    padding: 0.75rem 1rem;
    border-radius: var(--sn-radius-sm);
    background: var(--sn-panel);
    border: 1px solid var(--sn-line);
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);
    color: var(--sn-text);
    font-size: 0.85rem;
}

.busy-toast[hidden] {
    display: none;
}

/* --------------------------------------------------------------- /risk screen */

/*
 * A CSP violation found live, 2026-09-10 — two real inline `style="..."` attributes on
 * this screen, blocked by `style-src 'self'` (no `unsafe-inline`, no nonce on a style
 * attribute) since whichever day each was written. Neither has ever actually applied in
 * a real browser; only reading the CSP violation this session's own live click-through
 * surfaced made that visible; the HTML always looked correct on its own.
 */

/* The "Patch all" confirmation form, inside a `<details>` disclosure — wide enough that
   its own text and button do not look cramped against the table row it opens under. */
.risk-patch-all-form {
    min-width: 280px;
}

/* A saved view (ADR 65/118) — this screen's own tokens, not Bootstrap's generic
   `badge`/`text-bg-light`, the same reason `.sn-sev`/`.sn-badge` exist. Normal case and
   weight, unlike those two: a saved view's own name is a person's own words, not a
   status label. */
.risk-view-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.85rem;
    border-radius: var(--sn-radius-pill);
    border: 1px solid var(--sn-line);
    background: var(--sn-panel-2);
    font-size: 0.82rem;
}

.risk-view-chip a {
    color: var(--sn-text);
    text-decoration: none;
}

.risk-view-chip a:hover {
    text-decoration: underline;
}

.risk-view-chip-shared {
    font-size: 0.72rem;
    color: var(--sn-muted);
}

.risk-view-chip-delete {
    display: inline-flex;
    align-items: center;
    border: 0;
    background: none;
    padding: 0;
    line-height: 1;
    font-size: 1rem;
    color: var(--sn-muted);
}

.risk-view-chip-delete:hover {
    color: var(--sn-crit);
}

/* "Update all including production"'s own type-the-count confirmation (ADR 110) — a
   plain number field, not the page's full width. */
.confirm-count-input {
    max-width: 8rem;
}

/*
 * A long, unpaginated table scrolls inside its own box, not the whole page — a plain
 * `<table>` with no natural cap (every plugin in the fleet, the full update log, every
 * verification capture in a window) does not fit on one screen, and re-finding a scroll
 * position after every click was the operator's own real complaint (ADR 113). The
 * header row stays pinned via `position: sticky`, not JS.
 *
 * `border-collapse: separate` (Bootstrap's own default is `collapse`) plus a
 * `box-shadow` in place of `border-bottom` is the fix for a real gap found live (ADR
 * 114): with collapsed borders, a sticky `th`'s own background does not reliably paint
 * over the shared boundary line as content scrolls underneath it, leaving a hairline
 * sliver of whatever just scrolled past visible above the header. A `box-shadow` is
 * drawn inside the element's own layer, so it moves with the sticky position with no
 * such gap. The background still has to be explicit and match the enclosing `.panel`'s
 * own (`--sn-panel`), or rows would show through a transparent header as they scroll by.
 */
.table-scroll {
    max-height: 32rem;
    overflow-y: auto;
}

.table-scroll > table {
    border-collapse: separate;
    border-spacing: 0;
}

.table-scroll > table > thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--sn-panel);
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--sn-line);
}
