:root {
    --bg: #f6f7f9;
    --surface: #ffffff;
    --border: #e3e6ea;
    --text: #1c2024;
    --muted: #6b7280;
    --accent: #2563eb;
    --ok: #15803d;
    --ok-bg: #e8f5ec;
    --warn: #b45309;
    --warn-bg: #fdf3e3;
    --bad: #b91c1c;
    --bad-bg: #fdecec;
    --radius: 10px;
    --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .04);
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #14161a;
        --surface: #1c1f24;
        --border: #2b2f36;
        --text: #e6e8eb;
        --muted: #9aa1ab;
        --accent: #6ea8fe;
        --ok: #4ade80;
        --ok-bg: #14301f;
        --warn: #fbbf24;
        --warn-bg: #33280f;
        --bad: #f87171;
        --bad-bg: #3a1a1a;
        --shadow: none;
    }
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .9em; }

/* ---------- chrome ---------- */

.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .85rem 1.5rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.brand-block { display: inline-flex; align-items: baseline; gap: .5rem; }
.brand {
    display: inline-flex;
    align-items: center;
    gap: .55rem;
    font-weight: 650;
    letter-spacing: -.01em;
    color: var(--text);
}
.brand:hover { text-decoration: none; }
/* Quiet on purpose: it answers "is my change live?" when you go looking, and
   stays out of the way the rest of the time. */
.brand-version {
    color: var(--muted);
    font-size: .75rem;
    font-variant-numeric: tabular-nums;
    cursor: default;
}
.brand-dot {
    width: .6rem; height: .6rem;
    border-radius: 50%;
    background: var(--ok);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent);
}

.topbar-left { display: flex; align-items: center; gap: 1.5rem; }
.topbar-nav { display: flex; align-items: center; gap: .25rem; }
.topbar-actions { display: flex; align-items: center; gap: .5rem; }
/* Two groups plus the buttons: a phone is too narrow for one row. */
@media (max-width: 640px) {
    .topbar { flex-wrap: wrap; }
    .topbar-left { gap: .75rem; }
    .topbar-actions { flex-wrap: wrap; justify-content: flex-end; }
}
/* The signed-in address is a link to the one thing it has settings for. Quiet
   until it is pointed at: it is a label first and a link second. */
.signed-in { color: var(--muted); font-size: .85rem; text-decoration: none; }
a.signed-in:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.nav-link {
    padding: .45rem .6rem;
    border-radius: 8px;
    color: var(--muted);
    font-size: .875rem;
    font-weight: 550;
}
.nav-link:hover { color: var(--text); text-decoration: none; }
.nav-link.is-active { color: var(--text); background: var(--bg); }
@media (max-width: 640px) { .signed-in { display: none; } }

.wrap { max-width: 1360px; margin: 0 auto; padding: 1.75rem 1.5rem 3rem; }

.footer {
    padding: 1.5rem;
    text-align: center;
    color: var(--muted);
    font-size: .85rem;
}

/* ---------- buttons ---------- */

.button {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .45rem .8rem;
    border: 1px solid transparent;
    border-radius: 8px;
    background: var(--accent);
    color: #fff;
    font: inherit;
    font-size: .875rem;
    font-weight: 550;
    cursor: pointer;
}
.button:hover { filter: brightness(1.06); text-decoration: none; }
.button.ghost { background: transparent; color: var(--text); border-color: var(--border); }
.button.ghost:hover { background: var(--bg); filter: none; }
.button.tiny { padding: .25rem .5rem; font-size: 1rem; line-height: 1; }
/* Installing is not re-checking. It carries the same colour as the versions it
   is there to fix, so the eye connects the two without reading either. */
.button.update { background: var(--warn); border-color: transparent; color: #fff; }
/* Off production nothing reaches the mini server, so the buttons that would ask
   it for something stay where they are and stop working: the page is laid out
   while developing exactly as it is live. */
.button:disabled, .link-danger:disabled { opacity: .45; cursor: not-allowed; }
.button:disabled:hover { filter: none; }
.link-danger:disabled:hover { text-decoration: none; }
.inline { display: inline; }

/* ---------- summary ---------- */

.summary {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: .85rem;
    margin-bottom: 1.75rem;
}

.summary-card {
    display: flex;
    flex-direction: column;
    gap: .15rem;
    padding: .9rem 1.1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.summary-value { font-size: 1.7rem; font-weight: 650; line-height: 1.1; letter-spacing: -.02em; }
.summary-label { color: var(--muted); font-size: .82rem; }
.summary-card.is-ok .summary-value { color: var(--ok); }
.summary-card.is-outdated .summary-value { color: var(--warn); }
.summary-card.is-error .summary-value { color: var(--bad); }

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

.tiles {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
}

@media (max-width: 900px) {
    .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 600px) {
    .tiles { grid-template-columns: 1fr; }
}

.tile {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: .85rem;
    padding: 1rem 1.1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    transition: border-color .12s ease, transform .12s ease;
}
.tile:hover { transform: translateY(-1px); }
.tile:has(.tile-link:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

.tile.status-ok { border-left-color: var(--ok); }
.tile.status-outdated,
.tile.status-warning { border-left-color: var(--warn); }
.tile.status-error { border-left-color: var(--bad); }

.tile-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .6rem;
}

.tile-name {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -.01em;
    line-height: 1.3;
    overflow-wrap: anywhere;
}
.tile-link { color: var(--text); }
.tile-link:hover { text-decoration: none; color: var(--accent); }
/* Whole tile is clickable, without nesting the refresh form inside a link. */
.tile-link::after { content: ""; position: absolute; inset: 0; border-radius: var(--radius); }

.tile-body { flex: 1; }

.tile-message { margin: 0; font-size: .875rem; overflow-wrap: anywhere; }
.tile-error { color: var(--bad); }

.tile-primary {
    margin: 0 0 .7rem;
    font-size: .95rem;
    display: flex;
    align-items: baseline;
    gap: .35rem;
    flex-wrap: wrap;
}
.tile-primary-name { color: var(--muted); font-size: .8rem; margin-right: .15rem; }

.tile-stats {
    display: flex;
    gap: 1.4rem;
    margin: 0;
    flex-wrap: wrap;
}
.tile-stats dt {
    color: var(--muted);
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .05em;
}
.tile-stats dd { margin: .1rem 0 0; font-size: .9rem; font-variant-numeric: tabular-nums; }

.tile-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding-top: .7rem;
    border-top: 1px solid var(--border);
    font-size: .78rem;
}
/* Lift the button above the stretched link so it stays clickable. */
.tile-refresh { position: relative; z-index: 1; line-height: 1; }

/* ---------- backup ---------- */

.backup-grid .col-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.backup-grid .col-time { white-space: nowrap; }
.backup-grid tr.is-same-day td { background: color-mix(in srgb, var(--muted) 10%, transparent); }
/* Delete on a run (D35): a plain red word; the day's only backup asks in the row first. */
.link-danger { padding: 0; border: 0; background: none; color: var(--bad); font: inherit; font-size: .85rem; cursor: pointer; list-style: none; }
.link-danger:hover { text-decoration: underline; }
summary.link-danger::-webkit-details-marker { display: none; }
.link-marked { color: var(--muted); font-size: .85rem; cursor: pointer; list-style: none; text-decoration: underline dotted; text-underline-offset: 3px; }
.link-marked::-webkit-details-marker { display: none; }
.link-marked:hover { color: var(--text); }
/* The same label without the summary it opens: off production it only reports
   what production is about to do. */
span.link-marked { cursor: default; }
.backup-grid td.col-action { position: relative; }
/* Delete shows on the row under the pointer (or with keyboard focus in it); "Deleting" and an open question always. */
.run-delete .link-danger { visibility: hidden; }
.backup-grid tr:hover .run-delete .link-danger, .backup-grid tr:focus-within .run-delete .link-danger { visibility: visible; }
@media (hover: none) { .run-delete .link-danger { visibility: visible; } }
.inline-confirm[open] > summary { visibility: hidden; }
.inline-confirm-box { position: absolute; right: .75rem; top: 50%; transform: translateY(-50%); z-index: 2; display: inline-flex; align-items: center; gap: .5rem; padding: .35rem .5rem .35rem .75rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); box-shadow: var(--shadow); white-space: nowrap; font-size: .85rem; }
.button.danger { background: var(--bad); border-color: var(--bad); color: #fff; }
.button.tiny.danger, .inline-confirm-box .button.tiny { font-size: .8rem; padding: .25rem .55rem; }
.backup-grid tr:target td { background: color-mix(in srgb, var(--accent) 10%, transparent); }

/* One band per day, a dot per run, placed by its exact time. */
.timeline {
    padding: .75rem .75rem .4rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.timeline-track {
    position: relative;
    display: flex;
    height: 2.25rem;
    border-radius: 6px;
    background: var(--bg);
}
.timeline-day { flex: 1; border-left: 1px solid var(--border); }
.timeline-day:first-child { border-left: 0; }
.timeline-day.is-empty { background: color-mix(in srgb, var(--bad) 14%, transparent); }
/* A day with exactly one run shows that run's tooltip anywhere over the day. */
.timeline-day.is-single { position: relative; }
.timeline-day.is-single:hover,
.timeline-day.is-single:focus { z-index: 4; background: color-mix(in srgb, var(--accent) 8%, transparent); }
.timeline-day.is-single:hover .timeline-tip,
.timeline-day.is-single:focus .timeline-tip { display: flex; }
.timeline-day.is-start .timeline-tip { left: -.4rem; transform: none; }
.timeline-day.is-end .timeline-tip { left: auto; right: -.4rem; transform: none; }
.timeline-mark {
    position: absolute;
    top: 50%;
    width: .7rem;
    height: .7rem;
    border-radius: 50%;
    background: var(--ok);
    box-shadow: 0 0 0 2px var(--surface);
    transform: translate(-50%, -50%);
}
/* Where dots still touch, the backup that worked stays on top. */
.timeline-mark { z-index: 2; }
.timeline-mark.is-failed { z-index: 1; background: var(--bad); }
.timeline-mark:hover,
.timeline-mark:focus { z-index: 4; width: .95rem; height: .95rem; }
/* Instant, unlike a title attribute, and with room for more than one line.
   Focus as well as hover, so a tap on a phone opens it too. */
.timeline-tip {
    position: absolute;
    bottom: calc(100% + .55rem);
    left: 50%;
    z-index: 5;
    display: none;
    flex-direction: column;
    gap: .15rem;
    width: max-content;
    max-width: min(23rem, 85vw);
    padding: .5rem .65rem;
    border-radius: 8px;
    background: var(--text);
    color: var(--surface);
    font-size: .78rem;
    line-height: 1.35;
    text-align: left;
    white-space: normal;
    box-shadow: 0 4px 14px rgba(16, 24, 40, .18);
    transform: translateX(-50%);
    pointer-events: none;
}
.timeline-tip code { font-size: .72rem; opacity: .8; overflow-wrap: anywhere; }
.timeline-tip-ok { color: color-mix(in srgb, var(--ok) 55%, var(--surface)); font-weight: 600; }
.timeline-tip-failed { color: color-mix(in srgb, var(--bad) 45%, var(--surface)); font-weight: 600; }
.timeline-mark:hover .timeline-tip,
.timeline-mark:focus-visible .timeline-tip,
.timeline-mark:focus .timeline-tip { display: flex; }
.timeline-mark.is-start .timeline-tip { left: -.4rem; transform: none; }
.timeline-mark.is-end .timeline-tip { left: auto; right: -.4rem; transform: none; }
/* On a phone the tooltip is as wide as the track and starts where it starts:
   the dot or day starts at --pos percent of the track (100cqw); --shift
   undoes a dot's centring (50% of its width), a day has none. */
@media (max-width: 640px) {
    .timeline-track { container-type: inline-size; }
    .timeline-track .timeline-tip,
    .timeline-track .is-start .timeline-tip,
    .timeline-track .is-end .timeline-tip {
        left: calc(var(--pos) * -1cqw + var(--shift, 50%));
        right: auto;
        width: 100cqw;
        max-width: none;
        transform: none;
    }
}
.timeline-labels { display: flex; margin-top: .3rem; }
.timeline-labels span {
    flex: 1;
    text-align: center;
    font-size: .68rem;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
/* Every other day on a phone, or the numbers run into each other. */
@media (max-width: 640px) { .timeline-labels span:nth-child(even) { visibility: hidden; } }
.timeline-key {
    display: inline-block;
    width: .6rem;
    height: .6rem;
    margin: 0 .2rem 0 .6rem;
    border-radius: 50%;
    background: var(--ok);
    vertical-align: -1px;
}
.timeline-key:first-child { margin-left: 0; }
.timeline-key.is-failed { background: var(--bad); }
.timeline-key.is-empty { border-radius: 2px; background: color-mix(in srgb, var(--bad) 14%, transparent); }

/* ---------- tabs ---------- */

.tabs {
    display: flex;
    gap: .25rem;
    margin-bottom: 1.5rem;
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    overflow-y: hidden;
}
.tab {
    padding: .55rem .9rem;
    margin-bottom: -1px;
    border-bottom: 2px solid transparent;
    color: var(--muted);
    font-size: .9rem;
    font-weight: 550;
    white-space: nowrap;
}
.tab:hover { color: var(--text); text-decoration: none; }
.tab.is-active { color: var(--text); border-bottom-color: var(--accent); }
.tabs-action { margin-left: auto; align-self: center; padding: 0 0 .4rem .5rem; }
/* Where production has "Back up all now": when this copy last fetched its data,
   and the button that fetches it again. */
.prod-sync { display: flex; align-items: center; gap: .6rem; }
.prod-sync .muted { font-size: .8125rem; }
.page-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: .5rem; margin-bottom: 1.25rem; }

/* ---------- prose ---------- */

.prose { max-width: 46rem; line-height: 1.6; }
.prose h2:first-child { margin-top: 0; }
.prose p, .prose ul, .prose ol { margin: 0 0 1rem; }
.prose li { margin-bottom: .3rem; }
.prose .table-scroll { margin-bottom: 1rem; }

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

.group-title {
    margin: 2rem 0 .75rem;
    font-size: .95rem;
    font-weight: 600;
    letter-spacing: .01em;
    text-transform: uppercase;
    color: var(--muted);
}
.group-title .muted { text-transform: none; font-weight: 400; }

.table-scroll {
    overflow-x: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

table.grid {
    width: 100%;
    border-collapse: collapse;
    font-size: .9rem;
}

table.grid th {
    text-align: left;
    padding: .65rem .9rem;
    font-size: .74rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

table.grid td {
    padding: .7rem .9rem;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}
table.grid tbody tr:last-child td { border-bottom: 0; }
table.grid tbody tr:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }

/* Dashboard columns: fixed-ish widths and tabular figures so the version
   numbers line up vertically and can be scanned down the column. */
.dashboard-grid td,
.dashboard-grid th { white-space: nowrap; }
.dashboard-grid .col-project { width: 100%; white-space: normal; }
.dashboard-grid .col-project a { font-weight: 550; }
.dashboard-grid .col-status { width: 1%; }
.dashboard-grid .col-version,
.dashboard-grid .col-num,
.dashboard-grid .col-when {
    width: 1%;
    font-variant-numeric: tabular-nums;
}
.dashboard-grid .col-num { text-align: right; }
.dashboard-grid .col-action { width: 1%; text-align: right; white-space: nowrap; }
.dashboard-grid .col-action form + form { margin-left: .25rem; }
.dashboard-grid th.col-num { text-align: right; }
/* The grouped header: a band naming what the three columns under it are.
   Centred over them, and underlined for exactly their width — the rule is what
   says where the group starts and stops, which a floating word cannot. */
.dashboard-grid th.col-group {
    text-align: center;
    padding-bottom: .2rem;
    color: var(--text);
    border-bottom: 1px solid var(--muted);
}
/* The names under the band sit on a tint, so the group is a block the eye can
   see at a glance rather than something it has to trace from a rule. The tint
   stops where the group stops, which is the point. --bg against the table's
   --surface, so it follows the light and dark themes by itself. */
.dashboard-grid th.col-sub {
    padding-top: .25rem;
    background: var(--bg);
}
.dashboard-grid th.col-sub:first-of-type { border-top-left-radius: 6px; }
.dashboard-grid th.col-sub:last-of-type { border-top-right-radius: 6px; }
/* Every column name on one baseline, so the row of names reads as one row and
   the band is clearly a heading above three of them, rather than a second rank
   of headings the eye has to sort out. */
.dashboard-grid thead th[rowspan] { vertical-align: bottom; }
.cell-error { color: var(--bad); white-space: normal !important; }

.cell-project { min-width: 15rem; }
.project-link { font-weight: 550; display: inline-flex; align-items: center; gap: .55rem; }

/* ---------- site logos ---------- */

/*
 * No plate, no frame — the artwork sits straight on the page.
 *
 * The slot is wider than it is tall because half the logos are wordmarks, which
 * would shrink to an illegible strip in a square box. Every logo is centred in
 * that fixed slot, so the project names still line up whatever its shape.
 */
.logo {
    flex: none;
    width: 110px;
    height: 44px;
    object-fit: contain;
    object-position: center;
}
.logo-fallback {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 9px;
    background: hsl(var(--logo-hue, 210) 45% 92%);
    color: hsl(var(--logo-hue, 210) 45% 30%);
    font-size: 1.4rem;
    font-weight: 650;
    line-height: 1;
}
.logo-sm { width: 22px; height: 22px; border-radius: 6px; font-size: .75rem; }
.logo-sm.logo-wide { width: 44px; }
.logo-lg { width: 80px; height: 80px; }
.logo-lg.logo-fallback { border-radius: 16px; font-size: 2.2rem; }
/* Anything at least twice as wide as it is tall is a wordmark: on the detail
   page it gets twice the room, so it ends up drawn at twice the size. */
.logo-lg.logo-wide { width: 160px; }
.cell-note { margin-top: .2rem; color: var(--muted); font-size: .78rem; max-width: 26rem; }
.cell-actions { text-align: right; }
.muted { color: var(--muted); }
.warn { color: var(--warn); }

/* ---------- status ---------- */

.pill {
    display: inline-block;
    padding: .15rem .5rem;
    border-radius: 999px;
    font-size: .74rem;
    font-weight: 600;
    white-space: nowrap;
    background: var(--bg);
    color: var(--muted);
    border: 1px solid var(--border);
}
.pill.status-ok { background: var(--ok-bg); color: var(--ok); border-color: transparent; }
.pill.status-outdated { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
.pill.status-warning { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
.pill.status-error { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
/* The latest backup run on /backup (feature 214, i/backup-progress-e.html). */
.pill.status-queued { border-style: dashed; }
.backup-run { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .9rem; margin: 0 0 .75rem; padding: 0 .25rem; }
.backup-run strong { font-weight: 600; }
.backup-run.is-idle { color: var(--muted); }
.backup-run-spacer { flex: 1; }
.backup-run-eta { color: var(--accent); font-weight: 600; }
.backup-run-eta.is-late { color: var(--warn); }
.backup-grid .col-status { width: 9rem; }
/* Nine columns in the overview: a little less air per cell, so they fit without scrolling. */
table.grid.backup-grid th, table.grid.backup-grid td { padding-left: .7rem; padding-right: .7rem; }
tr.backup-running td { background: color-mix(in srgb, var(--accent) 6%, transparent); }
tr.backup-running td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
tr.backup-running.is-late td { background: color-mix(in srgb, var(--warn) 7%, transparent); }
tr.backup-running.is-late td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
.backup-cellbar { display: flex; align-items: center; gap: .6rem; }
.backup-bar { flex: 0 0 3.5rem; height: 6px; position: relative; overflow: hidden; background: color-mix(in srgb, var(--accent) 14%, transparent); }
.backup-bar > b { position: absolute; inset: 0 auto 0 0; background: var(--accent); transition: width .5s linear; }
/* Full but not done: the bar stops claiming precision and moves instead. */
.backup-bar.is-finishing > b { width: 100% !important; background: repeating-linear-gradient(-45deg, var(--accent) 0 8px, color-mix(in srgb, var(--accent) 65%, white) 8px 16px); background-size: 23px 23px; animation: backup-stripes .8s linear infinite; }
.backup-bar.is-late > b { background: repeating-linear-gradient(-45deg, var(--warn) 0 8px, color-mix(in srgb, var(--warn) 65%, white) 8px 16px); background-size: 23px 23px; }
@keyframes backup-stripes { to { background-position: 23px 0; } }
.backup-pct { font-size: .8rem; font-weight: 600; color: var(--accent); white-space: nowrap; }
tr.backup-running.is-late .backup-pct { color: var(--warn); }
.backup-dur { white-space: nowrap; }
/* The last 30 days of one project, one square per day (oldest left). */
.backup-grid td:first-child { white-space: nowrap; }
/* The backup icon in the dashboard (feature 214, S04): a small round badge next to the status pill. */
.col-status:has(.backup-icon) { white-space: nowrap; }
.backup-icon { margin-left: .35rem; display: inline-grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; vertical-align: middle; }
.backup-icon svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.backup-icon.is-ok { background: var(--ok-bg); color: var(--ok); }
.backup-icon.is-without_dump { background: var(--warn-bg); color: var(--warn); }
.backup-icon.is-failed, .backup-icon.is-overdue { background: var(--bad-bg); color: var(--bad); }
.backup-days { display: inline-flex; vertical-align: middle; }
/* The 2px gap is padding outside the painted box, so hovering it still shows the day. */
.backup-days i { box-sizing: content-box; width: 8px; height: 14px; padding: 0 1px; background-clip: content-box; background-color: color-mix(in srgb, var(--muted) 22%, transparent); }
.backup-days i.is-ok { background-color: var(--ok); }
.backup-days i.is-failed { background-color: var(--bad); }
.backup-usual { color: var(--muted); font-size: .8rem; margin-left: .3rem; }
.backup-usual.is-faster { color: var(--ok); }
.backup-usual.is-slower { color: var(--warn); }

/* "Back up all now" and "Back up now" (D33): the request waiting for the mini server, in the status column. */
.pill.status-requested { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); border-color: transparent; }
.pill.status-requested.is-stuck { background: var(--warn-bg); color: var(--warn); }
.backup-grid .col-action { width: 1%; text-align: right; white-space: nowrap; }
.backup-system-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: 0 0 1rem; }

/* The steps of a project's latest turn, opened from the toggle in its row. */
.backup-toggle, .backup-toggle-space { display: inline-block; width: 1.3rem; height: 1.3rem; margin: 0 .3rem 0 -.35rem; vertical-align: middle; }
.backup-toggle { position: relative; padding: 0; border: 0; border-radius: 5px; background: none; color: var(--muted); cursor: pointer; }
.backup-toggle::before { content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; margin: -4px 0 0 -4px; border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(-45deg); transition: transform .15s; }
.backup-toggle[aria-expanded="true"]::before { margin: -5px 0 0 -3px; transform: rotate(45deg); }
.backup-toggle:hover { background: color-mix(in srgb, var(--muted) 14%, transparent); color: var(--text); }
.backup-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.backup-grid tr.backup-steps-row > td { padding: .5rem 1rem .9rem 2.6rem; background: color-mix(in srgb, var(--muted) 5%, transparent); white-space: normal; }
.backup-grid tr.backup-steps-row > td:first-child { box-shadow: none; }
/* Sticky, so on a phone, where the table scrolls sideways, the panel stays in view. */
.backup-steps-panel { position: sticky; left: 1rem; max-width: min(46rem, calc(100vw - 5rem)); }
.backup-steps-head { font-size: .8rem; margin-bottom: .35rem; }
.backup-steps { list-style: none; margin: 0; padding: 0; }
.backup-step { display: grid; grid-template-columns: 1.4rem 9.5rem 1fr auto; align-items: center; gap: .6rem; padding: .32rem 0; font-size: .875rem; }
.backup-step + .backup-step { border-top: 1px solid var(--border); }
.backup-step-name { font-weight: 600; }
.backup-step-detail { color: var(--muted); overflow-wrap: anywhere; }
/* The file copy's bar, against how long the last copy took; stripes once past it. */
.backup-step-bar { display: block; height: 4px; margin-top: .3rem; position: relative; overflow: hidden; background: color-mix(in srgb, var(--accent) 14%, transparent); }
.backup-step-bar > b { position: absolute; inset: 0 auto 0 0; background: var(--accent); transition: width .5s linear; }
.backup-step-bar.is-finishing > b { width: 100% !important; background: repeating-linear-gradient(-45deg, var(--accent) 0 8px, color-mix(in srgb, var(--accent) 65%, white) 8px 16px); background-size: 23px 23px; animation: backup-stripes .8s linear infinite; }
.backup-step-time { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }
.backup-step-icon { display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; font-size: 11px; font-weight: 700; line-height: 1; }
.backup-step.is-ok .backup-step-icon { background: var(--ok-bg); color: var(--ok); }
.backup-step.is-ok .backup-step-icon::before { content: "✓"; }
.backup-step.is-warn .backup-step-icon { background: var(--warn-bg); color: var(--warn); }
.backup-step.is-warn .backup-step-icon::before { content: "!"; }
.backup-step.is-warn .backup-step-detail { color: var(--warn); }
.backup-step.is-failed .backup-step-icon { background: var(--bad-bg); color: var(--bad); }
.backup-step.is-failed .backup-step-icon::before { content: "✕"; }
.backup-step.is-failed .backup-step-detail { color: var(--bad); }
.backup-step.is-running .backup-step-icon { box-sizing: border-box; border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent); animation: backup-spin .9s linear infinite; }
.backup-step.is-running .backup-step-name, .backup-step.is-running .backup-step-time { color: var(--accent); }
.backup-step.is-pending .backup-step-icon { box-sizing: border-box; border: 2px solid var(--border); }
.backup-step.is-pending, .backup-step.is-skipped { color: var(--muted); }
.backup-step.is-pending .backup-step-name, .backup-step.is-skipped .backup-step-name { font-weight: 400; }
.backup-step.is-skipped .backup-step-icon::before { content: "-"; color: var(--muted); }
@keyframes backup-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .backup-step.is-running .backup-step-icon { animation-duration: 2.4s; }
}
@media (max-width: 640px) {
    .backup-grid tr.backup-steps-row > td { padding-left: 1rem; }
    .backup-step { grid-template-columns: 1.4rem 1fr auto; }
    .backup-step-detail { grid-column: 2 / -1; grid-row: 2; }
}

tr.status-error td:first-child { box-shadow: inset 3px 0 0 var(--bad); }
tr.status-outdated td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
tr.status-warning td:first-child { box-shadow: inset 3px 0 0 var(--warn); }

.badge {
    display: inline-block;
    margin-left: .35rem;
    padding: .05rem .35rem;
    border-radius: 5px;
    font-size: .68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    background: var(--bg);
    color: var(--muted);
    border: 1px solid var(--border);
}
.badge-muted { text-transform: none; font-weight: 400; letter-spacing: 0; }

.version-from { color: var(--muted); text-decoration: line-through; }
.version-to { color: var(--warn); font-weight: 600; }
.version-ok { color: var(--text); }
.arrow { color: var(--muted); margin: 0 .15rem; }
.count-outdated { color: var(--warn); font-weight: 650; }

/* ---------- detail page ---------- */

.detail-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}
.detail-head h1 { margin: .35rem 0 .25rem; font-size: 1.5rem; letter-spacing: -.02em; }
.detail-title { display: flex; align-items: center; gap: .75rem; margin-bottom: .6rem; }
.detail-meta { margin: 0; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; font-size: .9rem; }
.detail-actions { display: flex; align-items: center; gap: .6rem; }
.back { font-size: .85rem; color: var(--muted); }

.facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: .85rem;
    margin: 0 0 .5rem;
    padding: 1rem 1.1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.facts dt { color: var(--muted); font-size: .76rem; text-transform: uppercase; letter-spacing: .04em; }
.facts dd { margin: .15rem 0 0; font-size: .92rem; }

/* ---------- events ---------- */

/*
 * One grid for the whole list rather than a flex row per entry: with a row per
 * container each row sized its own columns, so a long project id pushed that
 * row's text further right than its neighbours'. The list is the grid and the
 * items are display:contents, which lets every column be as wide as the widest
 * cell in it and start at the same x on every row — without hardcoding widths
 * that would break on a longer name.
 */
.events {
    list-style: none;
    margin: 0;
    padding: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    font-size: .875rem;

    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: 1.25rem;
    align-items: baseline;
}
.events--with-project { grid-template-columns: max-content max-content 1fr; }

.events li { display: contents; }
.events li > * {
    padding: .5rem 0;
    border-top: 1px solid var(--border);
    overflow-wrap: anywhere;
}
.events li > *:first-child { padding-left: 1rem; }
.events li > *:last-child { padding-right: 1rem; }
.events li:first-child > * { border-top: 0; }

.event-time { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.event-project { color: var(--muted); }

/* An outage row leads with the window it covers, so that column carries the
   colour instead of the text beside it. */
.event-down { color: var(--bad); }
.event-body .pill { margin-right: .35rem; }

/* Too narrow for columns: stack each entry instead of scrolling sideways. */
@media (max-width: 700px) {
    .events,
    .events--with-project { grid-template-columns: 1fr; }
    .events li > * { border-top: 0; padding: 0 1rem; }
    .events li > *:first-child { padding-top: .5rem; border-top: 1px solid var(--border); }
    .events li > *:last-child { padding-bottom: .5rem; }
    .events li:first-child > *:first-child { border-top: 0; }
}

/* ---------- messages & login ---------- */

/* ---------- update confirmation ---------- */

/* Sits where a browser dialog would have, except it is part of the page: it can
   list the sites and say what is about to happen to each, which confirm() never
   could. The warn colour is the one the pending versions already use. */
.confirm {
    margin-bottom: 1.5rem;
    border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
    border-left: 3px solid var(--warn);
    border-radius: 10px;
    background: var(--warn-bg);
}
.confirm-body, .confirm-progress { padding: 1rem 1.15rem; }
.confirm-title { margin: 0 0 .75rem; font-size: 1rem; color: var(--warn); }

.confirm-list { margin: 0 0 .85rem; padding: 0; list-style: none; display: grid; gap: .45rem; }
.confirm-list li { display: flex; align-items: center; gap: .55rem; flex-wrap: wrap; }
.confirm-site { font-weight: 600; }
.confirm-reason { color: var(--muted); font-size: .85rem; }
.confirm-note { margin: 0 0 .9rem; color: var(--muted); font-size: .85rem; max-width: 46rem; }
/* Replaces the install button off production: the panel still says what would
   happen, it just does not offer to do it. */
.confirm-blocked {
    margin: 0 0 .9rem;
    padding: .6rem .75rem;
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-size: .875rem;
    max-width: 46rem;
}
.confirm-blocked code { font-size: .8125rem; }
.confirm-actions { display: flex; align-items: center; gap: .5rem; }
.confirm-actions .button.ghost { background: transparent; }

/* Swapped in for the body above while the install runs. The request blocks for
   minutes, so without this the page just sits there looking broken. */
.confirm-progress { display: flex; align-items: flex-start; gap: .75rem; }
/* An author "display" beats the UA stylesheet's [hidden] rule whatever its
   specificity, so the attribute needs saying again here or the progress block
   shows before anything has been submitted. */
.confirm-progress[hidden], .confirm-body[hidden] { display: none; }
.confirm-progress-text { display: block; color: var(--warn); }
.confirm-progress-note {
    display: block;
    margin-top: .2rem;
    color: var(--muted);
    font-size: .85rem;
    max-width: 46rem;
}
.spinner {
    flex: none;
    width: 1.05rem; height: 1.05rem;
    margin-top: .1rem;
    border: 2px solid color-mix(in srgb, var(--warn) 30%, transparent);
    border-top-color: var(--warn);
    border-radius: 50%;
    animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 2.4s; }
}

.flashes { margin-bottom: 1rem; }

.flash {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .75rem;
    padding: .65rem .9rem;
    border-radius: 8px;
    margin-bottom: .5rem;
    font-size: .9rem;
    opacity: 1;
    transform: translateY(0);
    transition: opacity .35s ease, transform .35s ease;
}
.flash.is-leaving { opacity: 0; transform: translateY(-4px); }

.flash-close {
    flex: none;
    padding: 0 .15rem;
    border: 0;
    background: none;
    color: inherit;
    opacity: .55;
    font: inherit;
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
}
.flash-close:hover { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
    .flash { transition: none; }
}
.flash-success { background: var(--ok-bg); color: var(--ok); }
.flash-error { background: var(--bad-bg); color: var(--bad); }
.flash-warning { background: var(--warn-bg); color: var(--warn); }
.flash-info { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }

.empty {
    padding: 1.1rem;
    background: var(--surface);
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    color: var(--muted);
}

/* A caveat under a list, not a paragraph of its own weight. */
.note { margin: .5rem 0 0; color: var(--muted); font-size: .8rem; max-width: 46rem; }

.login {
    max-width: 24rem;
    margin: 3rem auto;
    padding: 1.75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.login h1 { margin: 0 0 1rem; font-size: 1.25rem; }
.login label { display: block; margin-bottom: .35rem; font-size: .85rem; color: var(--muted); }
.login input[type=email],
.login input[type=password],
.login input[type=text],
.login input:not([type]) {
    width: 100%;
    padding: .55rem .7rem;
    margin-bottom: 1rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
}
.login .button { width: 100%; justify-content: center; }

/* The eye that shows a password, inside the field at its right end. The field
   keeps its own margin on the wrapper, so the button can simply fill the
   height. Room is left for it in the padding, or a long password would run
   underneath. */
.field-eye { position: relative; display: block; margin-bottom: 1rem; }
/* Block, not the inline default: an inline input leaves the line box's room
   for descenders under it, the wrapper ends up taller than the field, and the
   button centred against the wrapper sits a few pixels low. */
.field-eye input { display: block; margin-bottom: 0; padding-right: 2.6rem; }
.eye {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 2.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
}
.eye:hover { color: var(--text); }
.eye svg { width: 1.15rem; height: 1.15rem; }

/* Changing the password: the sign-in panel, a little wider, with what the page
   does to the password written under the form rather than hidden in a tooltip. */
.account { max-width: 28rem; }
.account-who { margin: -.5rem 0 1.25rem; color: var(--muted); font-size: .85rem; }
.account-who strong { color: var(--text); font-weight: 550; }
.account-rules { margin: 1.25rem 0 0; padding-left: 1.1rem; color: var(--muted); font-size: .8rem; line-height: 1.55; }
.account-rules li { margin-top: .35rem; }
.account-tabs { margin: -.5rem 0 1rem; }
.account-state { margin: 0 0 1rem; font-size: .9rem; }
.account-state.is-on strong { color: var(--good); }
.account-state.is-off strong { color: var(--warn); }
.account-step { margin: 0 0 1rem; font-size: .9rem; }
.account-label { margin: 1.25rem 0 .35rem; font-size: .85rem; color: var(--muted); }
.account-key { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1rem; letter-spacing: .04em; }
.account-devices { margin: 0 0 .75rem; padding-left: 1.1rem; font-size: .85rem; line-height: 1.6; }
.account-inline { display: inline-block; margin-right: .5rem; }
.account-inline input { width: auto; }
.account-off { margin-top: 1.5rem; border-top: 1px solid var(--border); padding-top: .75rem; }
.account-off summary { font-size: .85rem; color: var(--bad); cursor: pointer; }
.account-off form { margin-top: .75rem; }

/* The setup code and the key beside it, so whichever way the phone is set up
   the other is right there. */
.qr-block { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; margin-bottom: 1.25rem; }
.qr { width: 10.5rem; height: 10.5rem; border-radius: 6px; flex: none; }
.qr-manual { min-width: 11rem; flex: 1; }
.qr-manual .account-label { margin-top: 0; }

/* Six digits read back off a phone: big enough to check at a glance, spaced
   like the code itself, and not stretched across the panel like a password. */
.code-input {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 1.4rem;
    letter-spacing: .25em;
    text-align: center;
    padding: .6rem .5rem;
}
.account .code-input { max-width: 11rem; }

/* Activate next to the way out of the setup, side by side and clear of the
   field above: the field and the button that reads it should not look like one
   control. Width auto beats the panel's full-width buttons, which are for a
   form with one obvious action. */
.account-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; margin-top: 1.25rem; }
.account-actions .button { width: auto; }
.login .account-actions .button { width: auto; }

/* The "remember this browser" box: a line, not a form field with a frame. */
.checkline { display: flex; align-items: center; gap: .5rem; margin-bottom: 1rem; font-size: .85rem; color: var(--text); }
.checkline input { width: auto; margin: 0; }
.login-step { margin: 0 0 1rem; font-size: .9rem; color: var(--muted); }

/* Administrators by login on the project page, each with its 2FA badge. */
.admin-logins { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .75rem; }
.admin-login { white-space: nowrap; }
.badge.warn { color: var(--warn); }

/* Admins column: one admin per line, a missing second factor marked left of the name. */
.admin-logins, .admin-login { display: block; }
.admin-login { white-space: nowrap; }
.admin-login .badge { margin-right: .35rem; vertical-align: 1px; }
.num { font-variant-numeric: tabular-nums; }
/* Text for screen readers only. */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* The instant tooltip for every title on the page (templates/layout.php). */
.tip {
    position: fixed;
    z-index: 100;
    max-width: min(23rem, calc(100vw - 16px));
    padding: .35rem .55rem;
    border-radius: 6px;
    background: var(--text);
    color: var(--surface);
    font-size: .78rem;
    line-height: 1.35;
    white-space: pre-line;
    pointer-events: none;
}
.tip[hidden] { display: none; }
