/* Plain CSS, served from this origin. No CDN, no webfont, no external anything:
   the Content-Security-Policy is default-src 'none' with 'self' added back only
   where it is used, and a remote asset would mean loosening it.

   style-src is 'self' with NO 'unsafe-inline', so there is not a single style
   attribute anywhere in the application. Everything that looks dynamic — chart
   segments, meters, bars — is SVG geometry expressed as presentation
   attributes, with colour resolved by the classes below. */

/* ═══════════════════════════════════════════════════════════ design tokens */

:root {
    color-scheme: light dark;

    --bg: #f7f8fa;
    --surface: #ffffff;
    --surface-2: #f1f3f6;
    --fg: #12161c;
    --fg-soft: #47515e;
    --muted: #6b7684;
    --line: #dfe3e9;
    --line-strong: #c3cad4;

    --accent: #2456d8;
    --accent-soft: #e8eefc;
    --accent-fg: #ffffff;

    --ok: #17784a;
    --ok-soft: #e2f4ea;
    --warn: #a1620a;
    --warn-soft: #fdf0dc;
    --bad: #b4231f;
    --bad-soft: #fdeceb;
    --info: #1f6a86;
    --info-soft: #e3f2f8;

    --radius: 10px;
    --radius-sm: 6px;
    --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 1px 3px rgba(16, 24, 40, 0.06);
    --shadow-lg: 0 4px 12px rgba(16, 24, 40, 0.08);

    --sidebar-width: 216px;
    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Dark by system preference, then overridden either way by the toggle. The
   attribute selectors must win in BOTH directions, so a reader who prefers dark
   can still force light. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #0f1216;
        --surface: #161a20;
        --surface-2: #1d222a;
        --fg: #e8ebef;
        --fg-soft: #b3bcc7;
        --muted: #8793a1;
        --line: #272d36;
        --line-strong: #39424e;

        --accent: #6f9bff;
        --accent-soft: #1b2740;
        --accent-fg: #0f1216;

        --ok: #4ec98a;
        --ok-soft: #12301f;
        --warn: #e5a33f;
        --warn-soft: #302411;
        --bad: #ef6b66;
        --bad-soft: #341917;
        --info: #5fb6d4;
        --info-soft: #122a33;

        --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
        --shadow-lg: 0 4px 14px rgba(0, 0, 0, 0.45);
    }
}

:root[data-theme="dark"] {
    --bg: #0f1216;
    --surface: #161a20;
    --surface-2: #1d222a;
    --fg: #e8ebef;
    --fg-soft: #b3bcc7;
    --muted: #8793a1;
    --line: #272d36;
    --line-strong: #39424e;

    --accent: #6f9bff;
    --accent-soft: #1b2740;
    --accent-fg: #0f1216;

    --ok: #4ec98a;
    --ok-soft: #12301f;
    --warn: #e5a33f;
    --warn-soft: #302411;
    --bad: #ef6b66;
    --bad-soft: #341917;
    --info: #5fb6d4;
    --info-soft: #122a33;

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 4px 14px rgba(0, 0, 0, 0.45);
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
    margin: 0;
    font: 15px/1.55 var(--font);
    color: var(--fg);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { text-decoration-color: var(--accent); }
code, kbd { font-family: var(--mono); font-size: 0.9em; }
h1 { font-size: 1.5rem; letter-spacing: -0.015em; margin: 0 0 0.25rem; }
h2 { font-size: 1.1rem; letter-spacing: -0.01em; margin: 0 0 0.6rem; }
h3 { font-size: 0.95rem; margin: 0 0 0.4rem; }
p { margin: 0 0 0.75rem; }
.muted { color: var(--muted); font-size: 0.9em; }
.mono { font-family: var(--mono); }

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 3px;
}

/* ═══════════════════════════════════════════════════════════════ app shell */

.shell { display: grid; grid-template-columns: var(--sidebar-width) 1fr; min-height: 100vh; }

.sidebar {
    background: var(--surface);
    border-right: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    padding: 0.9rem 0.75rem;
    gap: 0.25rem;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
}

.brand {
    display: flex; align-items: center; gap: 0.5rem;
    font-weight: 650; font-size: 1rem; letter-spacing: -0.01em;
    color: var(--fg); text-decoration: none;
    padding: 0.35rem 0.5rem 0.9rem;
}
.brand-mark { width: 22px; height: 22px; flex: none; }

.side-nav { display: flex; flex-direction: column; gap: 1px; }
.side-nav a {
    display: flex; align-items: center; gap: 0.55rem;
    padding: 0.45rem 0.6rem;
    border-radius: var(--radius-sm);
    color: var(--fg-soft);
    text-decoration: none;
    font-size: 0.92rem;
}
.side-nav a:hover { background: var(--surface-2); color: var(--fg); }
.side-nav a[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 550; }
.side-nav .icon { width: 16px; height: 16px; flex: none; opacity: 0.85; }

.side-heading {
    font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--muted); padding: 1rem 0.6rem 0.3rem; font-weight: 600;
}

.side-foot { margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--line); }
.side-user { padding: 0.4rem 0.6rem; font-size: 0.85rem; }
.side-user strong { display: block; font-weight: 550; }
.side-actions { display: flex; gap: 0.4rem; align-items: center; padding: 0.35rem 0.5rem; }

main { padding: 1.5rem 1.75rem 4rem; max-width: 1500px; min-width: 0; }

.page-header {
    display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap;
    margin-bottom: 1.25rem;
}
.page-header .titles { margin-right: auto; min-width: 0; }
.page-header .subtitle { color: var(--muted); font-size: 0.9rem; margin: 0; }
.page-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.page-actions form { margin: 0; }

.breadcrumb { font-size: 0.85rem; color: var(--muted); margin-bottom: 0.5rem; }
.breadcrumb a { color: var(--muted); }
.breadcrumb a:hover { color: var(--accent); }

/* ═════════════════════════════════════════════════════════════════ cards */

.card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.1rem 1.2rem;
    min-width: 0;
}
.card > h2 { display: flex; align-items: baseline; gap: 0.5rem; }
.card > h2 .muted { font-weight: 400; }

.grid-cards { display: grid; gap: 1rem; margin-bottom: 1.25rem; }
.cols-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.cols-3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.cols-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

/* ══════════════════════════════════════════════════════════════ stat tiles */

.stat {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0.85rem 1rem;
    box-shadow: var(--shadow);
    display: flex; flex-direction: column; gap: 0.15rem;
    text-decoration: none; color: inherit;
}
a.stat:hover { border-color: var(--line-strong); box-shadow: var(--shadow-lg); }
.stat-label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); font-weight: 600; }
.stat-value { font-size: 1.65rem; font-weight: 620; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-note { font-size: 0.82rem; color: var(--muted); }
.stat-good .stat-value { color: var(--ok); }
.stat-warn .stat-value { color: var(--warn); }
.stat-bad .stat-value { color: var(--bad); }

/* ═════════════════════════════════════════════════════════════════ charts */

.chart { display: block; max-width: 100%; height: auto; }
.chart-row { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; }
.chart-donut { flex: none; }
.chart-centre { font: 620 1.35rem var(--font); fill: var(--fg); }
.chart-centre-sub { font: 400 0.72rem var(--font); fill: var(--muted); }

.bar { fill: var(--accent); }
.bar:hover { fill: var(--fg); }
.bar-label { font: 400 0.68rem var(--font); fill: var(--muted); }
.bar-value { font: 550 0.68rem var(--font); fill: var(--fg-soft); }

.chart-meter { width: 100%; height: 8px; }
.meter-track { fill: var(--surface-2); }
.fill-ok { fill: var(--ok); }
.fill-warn { fill: var(--warn); }
.fill-bad { fill: var(--bad); }
.fill-accent { fill: var(--accent); }

/* Donut segment colours. Named by bucket so the legend swatch and the arc
   cannot drift apart — both read the same custom property. */
.seg-2xx, .swatch-2xx, .seg-indexable, .swatch-indexable { --seg-colour: var(--ok); }
.seg-3xx, .swatch-3xx { --seg-colour: var(--warn); }
.seg-4xx, .swatch-4xx, .seg-blocked, .swatch-blocked { --seg-colour: var(--bad); }
.seg-5xx, .swatch-5xx { --seg-colour: #7c3aed; }
.seg-error, .swatch-error { --seg-colour: var(--info); }
.seg-skipped, .swatch-skipped, .seg-unknown, .swatch-unknown { --seg-colour: var(--line-strong); }

.seg { stroke: var(--seg-colour, var(--accent)); }
.seg:hover { stroke: var(--fg); }

.legend { list-style: none; padding: 0; margin: 0; flex: 1 1 200px; min-width: 190px; }
.legend li { border-bottom: 1px solid var(--line); }
.legend li:last-child { border-bottom: 0; }
.legend li > a, .legend li > span, .legend li { display: flex; }
.legend li > a {
    align-items: center; gap: 0.5rem; padding: 0.35rem 0.1rem;
    text-decoration: none; color: inherit; font-size: 0.88rem; width: 100%;
}
.legend li > a:hover { color: var(--accent); }
.legend-swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; background: var(--seg-colour, var(--accent)); }
.legend-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-value { font-variant-numeric: tabular-nums; font-weight: 550; }
.legend-share { color: var(--muted); font-size: 0.82em; min-width: 2.6rem; text-align: right; font-variant-numeric: tabular-nums; }

/* ═════════════════════════════════════════════════════════════════ tables */

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

.grid { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.88rem; }
.grid th, .grid td { padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--line); text-align: left; }
.grid tbody tr:last-child td { border-bottom: 0; }
.grid th {
    background: var(--surface-2); position: sticky; top: 0; z-index: 1;
    white-space: nowrap; font-size: 0.76rem; text-transform: uppercase;
    letter-spacing: 0.045em; color: var(--muted); font-weight: 650;
}
.grid th a { text-decoration: none; color: inherit; }
.grid th a:hover { color: var(--fg); }
.grid th a.sorted { color: var(--accent); }
.grid tbody tr:hover { background: var(--surface-2); }
.grid tr.inactive { opacity: 0.55; }
.grid .num, .grid .col-int, .grid .col-ms, .grid .col-inlinks { text-align: right; font-variant-numeric: tabular-nums; }
.empty { text-align: center; color: var(--muted); padding: 2.5rem 1rem; }
td.empty, .grid .empty { padding: 2.5rem 1rem; }

.col-url, .col-text { max-width: 30rem; overflow: hidden; text-overflow: ellipsis; }
.col-detail { white-space: nowrap; width: 1%; }
.col-detail a {
    font-size: 0.78rem; text-decoration: none; border: 1px solid var(--line);
    border-radius: 4px; padding: 0.1rem 0.4rem; color: var(--muted);
}
.col-detail a:hover { border-color: var(--accent); color: var(--accent); }

/* A value that failed the scheme allowlist renders as text, visibly not a
   link, so an operator can see what the page actually contained. */
.unlinkable { color: var(--muted); text-decoration: line-through dotted; }

/* ═══════════════════════════════════════════════════ status and messages */

.status {
    display: inline-block; padding: 0.1rem 0.45rem; border-radius: 999px;
    font-size: 0.76rem; font-weight: 620; font-variant-numeric: tabular-nums;
    background: var(--surface-2); color: var(--fg-soft);
}
.status-2xx, .status-completed { background: var(--ok-soft); color: var(--ok); }
.status-3xx, .status-running, .status-queued, .status-paused { background: var(--warn-soft); color: var(--warn); }
.status-4xx, .status-5xx, .status-failed, .status-cancelled { background: var(--bad-soft); color: var(--bad); }

.tag {
    font-size: 0.72rem; padding: 0.05rem 0.35rem; border: 1px solid var(--line);
    border-radius: 3px; color: var(--muted); white-space: nowrap;
}

/* ── resource type badges ──────────────────────────────────────────────────
   HTML is deliberately the quiet one. On a normal crawl almost every row is
   HTML, so colouring it would tint the whole table and leave nothing to scan
   for; the point of the column is spotting the handful of rows that are NOT
   pages. Both theme blocks below already redefine the --*-soft variables, so
   these inherit light and dark without a second rule. */
.restype {
    display: inline-block; padding: 0.1rem 0.45rem; border-radius: 999px;
    font-size: 0.76rem; font-weight: 620; white-space: nowrap;
    background: var(--surface-2); color: var(--muted);
}
.restype-pdf, .restype-document { background: var(--bad-soft); color: var(--bad); }
.restype-image, .restype-media { background: var(--info-soft); color: var(--info); }
.restype-css, .restype-js, .restype-font { background: var(--warn-soft); color: var(--warn); }
.restype-feed { background: var(--accent-soft); color: var(--accent); }
.restype-archive, .restype-other { background: var(--surface-2); color: var(--fg-soft); }

.restype-note {
    font-size: 0.7rem; color: var(--muted); white-space: nowrap; margin-left: 0.3rem;
}

.flash {
    padding: 0.65rem 0.9rem; border-radius: var(--radius-sm); margin-bottom: 1rem;
    border: 1px solid var(--line); background: var(--surface); font-size: 0.9rem;
}
.flash-success { border-color: var(--ok); background: var(--ok-soft); color: var(--ok); }
.flash-error { border-color: var(--bad); background: var(--bad-soft); color: var(--bad); }
.flash-info { border-color: var(--info); background: var(--info-soft); color: var(--info); }
.notice {
    border: 1px solid var(--warn); background: var(--warn-soft); color: var(--warn);
    padding: 0.65rem 0.9rem; border-radius: var(--radius-sm); font-size: 0.9rem;
    margin-bottom: 1rem;
}
/* For a notice that is not "be aware" but "this is stuck and will stay stuck".
   The default amber reads as informational, and the whole point of the worker
   warning is that the reassuring reading is the wrong one. */
.notice-bad { border-color: var(--bad); background: var(--bad-soft); color: var(--bad); }
.notice code { background: transparent; font-size: 0.88em; }

/* ══════════════════════════════════════════════════════════════════ forms */

.stack { display: flex; flex-direction: column; gap: 0.9rem; max-width: 640px; }
.form-wide { max-width: 860px; }
.stack label { display: flex; flex-direction: column; gap: 0.3rem; font-weight: 550; font-size: 0.9rem; }
/* flex-start, not center: an option whose description wraps to two lines should
   keep its control beside the FIRST line, not floating in the middle. */
.stack label.inline { flex-direction: row; align-items: flex-start; gap: 0.55rem; font-weight: 400; }
.stack label.inline:hover { color: var(--fg); }
.field-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.field-row label { flex: 1 1 150px; }

input, select, textarea {
    font: inherit; font-size: 0.92rem; padding: 0.45rem 0.6rem;
    border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--fg); width: 100%;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }
/* Both, not just checkboxes. The width: 100% above is for text fields; letting
   it reach a radio makes the control stretch across the row and shove its own
   label to the far edge. */
input[type="checkbox"], input[type="radio"] { width: auto; flex: none; margin: 0.2rem 0 0; }
textarea { resize: vertical; font-family: var(--mono); font-size: 0.85rem; }

button, .button {
    font: inherit; font-size: 0.88rem; font-weight: 550;
    padding: 0.45rem 0.85rem; border: 1px solid var(--accent);
    background: var(--accent); color: var(--accent-fg);
    border-radius: var(--radius-sm); cursor: pointer;
    text-decoration: none; display: inline-flex; align-items: center; gap: 0.4rem;
    white-space: nowrap;
}
button:hover, .button:hover { filter: brightness(1.08); }
button[disabled] { opacity: 0.5; cursor: not-allowed; }
button.secondary, .button.secondary {
    background: var(--surface); color: var(--fg); border-color: var(--line-strong);
}
button.secondary:hover, .button.secondary:hover { background: var(--surface-2); filter: none; }
button.danger { background: var(--bad); border-color: var(--bad); color: #fff; }
button.link { background: none; border: none; color: var(--accent); padding: 0; text-decoration: underline; }
button.icon-only { padding: 0.35rem; }

fieldset {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    /* A fieldset is not a flex container by default, so its options stacked
       with no spacing and the legend sat flush against the border. */
    padding: 0.5rem 1rem 0.9rem;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    min-width: 0;
}
legend {
    padding: 0 0.4rem;
    font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--muted); font-weight: 650;
}
/* Ten checkboxes stacked one per line pushed the rest of the form off the
   screen. auto-fit rather than a fixed count so it collapses to one column on a
   phone without a media query. */
.checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 0.35rem 1rem;
}
.checkbox-grid .inline { margin: 0; }

/* ══════════════════════════════════════════════ report search and facets */

/* A <details>, so collapsing needs no script — the CSP is script-src 'self'
   with no unsafe-inline, and this is exactly the case native HTML covers. */
.report-search {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    padding: 0.6rem 0.9rem;
    margin-bottom: 0.85rem;
}
/* The default <summary> marker is nearly invisible in several browsers and
   absent in Safari, so it is removed and replaced with an explicit chevron and
   a Show/Hide label. Both are CSS keyed off [open] — no script, which the CSP
   would block anyway. */
.report-search > summary,
.report-group > summary {
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--fg-soft);
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.15rem 0.1rem;
    border-radius: var(--radius-sm);
    list-style: none;
}
.report-search > summary::-webkit-details-marker,
.report-group > summary::-webkit-details-marker { display: none; }
.report-search > summary::marker,
.report-group > summary::marker { content: ""; }
.report-search > summary:hover,
.report-group > summary:hover { color: var(--accent); }
.report-search > summary:focus-visible,
.report-group > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.summary-chevron {
    display: inline-block;
    transition: transform 0.12s ease;
    color: var(--muted);
    font-size: 0.9em;
}
/* Keyed off the element, not each class that uses it. .report-search is also
   worn by a plain <form> in one or two places; details[open] cannot match those,
   which is exactly the behaviour wanted. */
details[open] > summary .summary-chevron { transform: rotate(90deg); }
.summary-title { flex: 0 0 auto; }

/* Says what clicking will DO, not what the state is — the same convention the
   theme toggle uses. */
.summary-toggle { margin-left: auto; font-weight: 400; font-size: 0.8rem; }
.summary-toggle::after { content: "Show"; }
details[open] > summary .summary-toggle::after { content: "Hide"; }

.report-search[open] > summary { margin-bottom: 0.7rem; }

/* The report groups inside the Reports card. Same mechanism as .report-search,
   but no box of its own: these sit stacked inside a card that already has one,
   and twelve nested borders would read as twelve separate panels. The count
   goes in the summary so a collapsed group still says how much is behind it. */
.report-group {
    border-top: 1px solid var(--line);
    padding: 0.35rem 0;
}
.report-group:first-of-type { border-top: 0; }
.report-group > summary { font-size: 0.9rem; color: var(--fg); }
.report-group[open] > summary { margin-bottom: 0.35rem; }
.report-group .summary-count {
    font-weight: 400;
    font-size: 0.8rem;
    color: var(--muted);
}

/* Landed here from a count on another report. */
.drill-banner {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    margin-bottom: 0.75rem;
    border-left: 3px solid var(--accent);
    background: var(--accent-soft);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    font-size: 0.85rem;
}
.drill-banner a { font-weight: 600; }
.report-search > form { display: flex; flex-direction: column; gap: 0.7rem; }

/* The search input takes whatever is left; the two selects stay their natural
   width so the mode never wraps away from the term it modifies. */
.search-row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.search-row input[type="search"] { flex: 1 1 18rem; min-width: 0; }
.search-row select { flex: 0 0 auto; }

/* auto-fit rather than a fixed column count, so this collapses to one column
   on a phone without a media query. */
.facet-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 0.55rem 0.9rem;
}
.facet { display: flex; flex-direction: column; gap: 0.2rem; margin: 0; min-width: 0; }
.facet-label { font-size: 0.78rem; color: var(--fg-soft); font-weight: 550; }
.facet-label .muted { font-weight: 400; }
.facet select, .facet input { width: 100%; min-width: 0; }

.range-row { display: flex; align-items: center; gap: 0.3rem; }
/* Both bounds share the row evenly; without a min-width of 0 the number
   inputs refuse to shrink and force the grid column wider than its track. */
.range-row input { flex: 1 1 0; min-width: 0; }

.search-actions { display: flex; gap: 0.5rem; align-items: center; }

/* Heading outline. One class per level rather than an inline style, because the
   CSP is style-src 'self' with no unsafe-inline and a style attribute would be
   blocked. Indentation is what makes an out-of-sequence heading visible.

   The cell is shrink-to-fit: width 1% plus nowrap is the standard way to tell a
   table to give a column only what it needs. Without it the browser sized the
   column to the deepest indent plus the badge and took nearly half the table,
   leaving the heading text — the part being read — squeezed into the rest. */
.grid td.heading-cell, .grid th.heading-cell { width: 1%; white-space: nowrap; }

.heading-level {
    display: inline-block;
    font-size: 0.78rem;
    /* Uniform width so the level chips line up in a column and the eye reads
       the indentation, not the varying width of the label. */
    min-width: 1.6rem;
}
.heading-level-2 { margin-left: 0.75rem; }
.heading-level-3 { margin-left: 1.5rem; }
.heading-level-4 { margin-left: 2.25rem; }
.heading-level-5 { margin-left: 3rem; }
.heading-level-6 { margin-left: 3.75rem; }

/* The H1 is the one an auditor is looking for, so it is the one that stands
   out — and its absence then reads as an absence. */
.heading-level-1 { color: var(--accent); font-weight: 700; }

.heading-position { font-size: 0.72rem; color: var(--muted); }

/* Term tables. Two columns on a wide screen, one on a narrow one, so the four
   phrase lengths can be compared side by side rather than scrolled between. */
.term-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
    gap: 1rem;
    margin-top: 0.75rem;
}
.term-block { min-width: 0; }
.term-block h3 { margin: 0 0 0.35rem; font-size: 0.85rem; color: var(--fg-soft); }
.term-block table { font-size: 0.85rem; }
/* The phrase itself is the column that needs room; the four numeric columns
   are two or three characters each. */
.term-block td:first-child { word-break: break-word; }
.term-block .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Readiness checklist. Badge and text on one row, text wrapping under itself
   rather than under the badge. */
.check-list { list-style: none; padding: 0; margin: 0.6rem 0; display: flex; flex-direction: column; gap: 0.4rem; }
.check { display: flex; align-items: baseline; gap: 0.55rem; font-size: 0.9rem; }
.check > .status { flex: 0 0 auto; }
.check-fail > span:last-child strong { color: var(--bad); }

/* The individual items behind a finding, listed under it. A count with no way
   to see what it counts is a finding nobody acts on. */
.check-items {
    list-style: none;
    margin: 0.35rem 0 0.2rem;
    padding: 0 0 0 0.85rem;
    border-left: 2px solid var(--line);
    display: flex; flex-direction: column; gap: 0.25rem;
}
.check-items li {
    font-size: 0.85rem;
    color: var(--fg-soft);
    overflow-wrap: anywhere;
}

/* Drill link on an aggregate cell. Underlined on hover only: on a table of
   counts, every number being a permanently underlined link is unreadable. */
.grid a.drill {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px dotted var(--line-strong);
}
.grid a.drill:hover { color: var(--accent); border-bottom-color: var(--accent); }

.inline-form { display: flex; align-items: flex-end; gap: 0.75rem; margin-bottom: 1rem; }
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.actions form { display: flex; gap: 0.25rem; align-items: center; }

/* ════════════════════════════════════════════════════════ summary blocks */

.summary dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.4rem 1.1rem; margin: 0; }
.summary dt { color: var(--muted); font-size: 0.85rem; }
.summary dd { margin: 0; min-width: 0; overflow-wrap: anywhere; font-size: 0.9rem; }

.report-list { list-style: none; padding: 0; margin: 0; }
.report-list li { padding: 0; border-bottom: 1px solid var(--line); }
.report-list li:last-child { border-bottom: 0; }
.report-list a { display: block; padding: 0.5rem 0.1rem; text-decoration: none; color: var(--fg); font-size: 0.9rem; }
.report-list a:hover { color: var(--accent); }
.report-list .muted { display: block; font-size: 0.82rem; }
.plain { list-style: none; padding: 0; }

.pagination { display: flex; gap: 0.75rem; align-items: center; padding: 0.85rem 0; font-size: 0.88rem; }
.pagination a { padding: 0.3rem 0.7rem; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); text-decoration: none; }
.pagination a:hover { background: var(--surface-2); }

/* ═════════════════════════════════════════════════════ URL audit specifics */

.url-title { font-size: 1.05rem; overflow-wrap: anywhere; line-height: 1.4; font-weight: 600; }
.url-title a { text-decoration: none; }
.chain { padding-left: 1.3rem; margin: 0; }
.chain li { padding: 0.2rem 0; }
.position-bar { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; font-size: 0.82rem; margin: 0.3rem 0 0.8rem; }
.pos { padding: 0.08rem 0.45rem; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); }
/* Struck through when its rows are being withheld, so the excluded links are
   visibly excluded rather than just absent. */
.pos-hidden { text-decoration: line-through; opacity: 0.65; }

section { margin-bottom: 1.5rem; }

/* ══════════════════════════════════════════════ audit section separation

   Inlinks and outlinks are structurally identical — a table of URLs with anchor
   text — and on a long page they read as the same thing twice. Three signals
   pull them apart, because one is easy to miss when scrolling:
   a coloured edge, a direction arrow in the heading, and a differently coloured
   first-column header on the table itself. */

.audit-section {
    border: 1px solid var(--line);
    border-left: 4px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow);
    padding: 1.1rem 1.25rem 1.25rem;
    margin-bottom: 1.1rem;
}
/* Each audit section is a <details>, so its heading lives in a <summary>. The
   whole bar is the click target — a chevron alone is a tiny thing to hit on a
   page made of thirteen of them. The rule below is why the heading styling is
   two levels deep now rather than a direct child. */
.audit-section > summary {
    cursor: pointer;
    display: flex; align-items: center; gap: 0.5rem;
    list-style: none;
    border-radius: var(--radius-sm);
}
.audit-section > summary::-webkit-details-marker { display: none; }
.audit-section > summary::marker { content: ""; }
.audit-section > summary:hover .summary-chevron { color: var(--accent); }
.audit-section > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.audit-section > summary > h2 {
    margin: 0;
    flex: 1 1 auto;
    display: flex; align-items: center; gap: 0.5rem;
}
/* The separator belongs to the OPEN state. On a closed section it would draw a
   rule under the title with nothing beneath it. */
.audit-section[open] > summary {
    padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--line);
    margin-bottom: 0.8rem;
}
.audit-section > summary > h2 .muted { font-weight: 400; margin-left: auto; }
.audit-icon { width: 17px; height: 17px; flex: none; }

.audit-in { border-left-color: var(--ok); }
.audit-in > summary > h2 { color: var(--ok); }
.audit-in .col-direction { color: var(--ok); }

.audit-out { border-left-color: var(--accent); }
.audit-out > summary > h2 { color: var(--accent); }
.audit-out .col-direction { color: var(--accent); }

.audit-meta { border-left-color: var(--info); }
.audit-warn { border-left-color: var(--warn); }
.audit-warn > summary > h2 { color: var(--warn); }

/* The first column header carries the direction word, coloured to match the
   section edge, so a screenshot of the table alone is still unambiguous. */
.grid th.col-direction { font-weight: 700; letter-spacing: 0.06em; }

/* ══════════════════════════════════════════════════════════════ live crawl */

.live-dot {
    display: inline-block; width: 7px; height: 7px; border-radius: 50%;
    background: var(--warn); margin-right: 0.35rem; vertical-align: middle;
}
@media (prefers-reduced-motion: no-preference) {
    .live-dot { animation: pulse 1.8s ease-in-out infinite; }
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

/* ══════════════════════════════════════════════════════════════════ login */

body.centred { display: grid; place-items: center; min-height: 100vh; background: var(--bg); }
body.centred .card { width: min(420px, 92vw); padding: 2rem; }

/* ═════════════════════════════════════════════════════════════ responsive */

@media (max-width: 900px) {
    .shell { grid-template-columns: 1fr; }
    .sidebar {
        position: static; height: auto; flex-direction: row; flex-wrap: wrap;
        align-items: center; border-right: 0; border-bottom: 1px solid var(--line);
    }
    .side-nav { flex-direction: row; flex-wrap: wrap; }
    .side-heading { display: none; }
    .side-foot { margin: 0 0 0 auto; padding: 0; border: 0; display: flex; align-items: center; gap: 0.5rem; }
    .side-user { display: none; }
    main { padding: 1rem; }
}

@media print {
    .sidebar, .page-actions, .pagination { display: none; }
    .shell { grid-template-columns: 1fr; }
    .card, .table-scroll { box-shadow: none; }
}

/* An engaged report filter. Uses the accent rather than a checkbox so the whole
   chip is the hit target and the state is legible at a glance. */
.pos-active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
a.pos { text-decoration: none; }
a.pos:hover { border-color: var(--line-strong); color: var(--fg); }

/* Theme toggle. Full-width and labelled rather than an icon in the corner: the
   icon-only version was invisible enough that the application looked as though
   it had no light mode. */
.theme-toggle {
    display: flex; align-items: center; gap: 0.55rem;
    width: 100%;
    padding: 0.45rem 0.6rem;
    margin-bottom: 0.35rem;
    background: none;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--fg-soft);
    font-size: 0.92rem; font-weight: 400;
    cursor: pointer;
}
.theme-toggle:hover { background: var(--surface-2); color: var(--fg); border-color: var(--line); filter: none; }
.theme-toggle .icon { width: 16px; height: 16px; flex: none; opacity: 0.85; }

/* ══════════════════════════════════════════════ directory tree diagram */

/* Server-rendered SVG, so every rule here is a class. The CSP is style-src
   'self' with no unsafe-inline: a style attribute inside the SVG would be
   blocked and the diagram would render as unstyled black boxes. */
.dirtree {
    display: block;
    width: 100%;
    height: auto;
    /* Stops a shallow tree being stretched to absurd node heights by the
       container. Wide trees scroll in .table-scroll instead. */
    max-width: 1200px;
    min-width: 640px;
    font-family: inherit;
}

.dirtree-edge {
    fill: none;
    stroke: var(--line-strong);
    stroke-width: 1;
}

.dirtree-box {
    fill: var(--surface-2);
    stroke: var(--line-strong);
    stroke-width: 1;
}
.dirtree-node a { cursor: pointer; }
.dirtree-node a:hover .dirtree-box { stroke: var(--accent); stroke-width: 2; }
.dirtree-node a:focus-visible .dirtree-box { stroke: var(--accent); stroke-width: 2; }

.dirtree-label {
    font-size: 11px;
    fill: var(--fg);
}
.dirtree-count {
    font-size: 10px;
    fill: var(--muted);
    text-anchor: end;
}

/* Health bands. Deliberately four steps rather than a gradient — a gradient
   invites reading precision into a colour, and the question this answers is
   "is anything wrong in here", not "is it 94% or 96%". */
.dirtree-ok .dirtree-box { stroke: var(--ok); }
.dirtree-mixed .dirtree-box { stroke: var(--info); }
.dirtree-warn .dirtree-box { stroke: var(--warn); }
.dirtree-bad .dirtree-box { stroke: var(--bad); stroke-width: 2; }

.dirtree-remainder .dirtree-box {
    fill: none;
    stroke: var(--line);
    stroke-dasharray: 3 3;
}
.dirtree-remainder .dirtree-label { fill: var(--muted); font-style: italic; }

.inline-stats {
    display: flex; flex-wrap: wrap; gap: 0.4rem 1.6rem;
    margin: 0; align-items: baseline;
}
.inline-stats dt { color: var(--muted); font-size: 0.82rem; }
.inline-stats dd { margin: 0 1rem 0 0; font-size: 1.05rem; font-weight: 600; }
