/* scva -- site-wide stylesheet.

   One hand-written stylesheet, no framework (task-7-brief.md guidance).
   Structure:
     1. Design tokens (colour, type, space) -- light theme, then a dark
        override under prefers-color-scheme.
     2. Reset / base element styles.
     3. Layout: header, nav, main.
     4. Components: cards, stat rows, forms, buttons, placeholders.
     5. Responsive: the nav wraps at tablet width rather than breaking.

   Tokens worth knowing about for later screens (Tasks 8-13):
     --color-pass / --color-fail / --color-review are reserved for verdict
     badges -- the review screen (Task 12) will want them next to a video,
     so they're chosen to hold up against dark surfaces, not just white.
     --focus-ring is the one focus style for the whole app; Task 12 is
     keyboard-driven end to end, so this is deliberately visible rather
     than the browser default.
*/

:root {
    color-scheme: light dark;

    /* Type scale -- 1.2 ratio (minor third), 16px base. */
    --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --text-xs: 0.8rem;
    --text-sm: 0.9rem;
    --text-base: 1rem;
    --text-md: 1.2rem;
    --text-lg: 1.44rem;
    --text-xl: 1.9rem;

    /* Space scale -- 4px base. */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;

    --radius: 8px;
    --border-width: 1px;
    --max-width: 72rem;

    /* Palette -- light. */
    --color-bg: #f7f7f5;
    --color-surface: #ffffff;
    --color-surface-raised: #ffffff;
    --color-border: #dcdad4;
    /* A step darker than --color-border, for a box that has to out-weigh
       the rules drawn inside it (TASK-70). */
    --color-result-border: #b9b5a9;
    --color-text: #1c1b19;
    --color-text-muted: #6b6860;
    --color-accent: #1f5c4d;
    /* The rule tag (TASK-95). Pale yellow rather than the accent green
       it started as: the accent is what every button on the site is
       painted with, so a green chip beside them read as one more control
       to press (TASK-101). Yellow is the one warm hue not already spoken
       for -- amber is awaiting-review, orange is skipped, and both of
       those are saturated fills on the progress bar rather than a pale
       tint behind small text. */
    --color-rule-bg: #faf4d9;
    --color-rule-border: #ddcd93;
    --color-rule-text: #5c4708;
    --color-accent-contrast: #ffffff;
    --color-header-bg: #14140f;
    --color-header-text: #f2f1ec;
    --color-header-text-muted: #b9b6ab;

    /* The decision card's ground, tinted by what was decided. Pale
       enough that the verdict word stays the loudest thing on the card
       -- the tint says which way it went before the word is read, it
       does not replace it. */
    --decision-pass: #eef6f1;
    --decision-fail: #fbeeed;
    --decision-review: #f6f0e3;
    --color-pass: #1f7a4d;
    --color-fail: #a3312b;
    --color-review: #9a6a12;

    --focus-ring: #2f6fed;

    --shadow-card: 0 1px 2px rgba(20, 20, 15, 0.06), 0 1px 1px rgba(20, 20, 15, 0.04);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-bg: #161613;
        --color-surface: #1f1f1b;
        --color-surface-raised: #262620;
        --color-border: #3a392f;
        --color-result-border: #59564a;
        --color-text: #ecebe4;
        --color-text-muted: #a6a397;
        --color-accent: #5fbf9d;
        --color-rule-bg: #302b18;
        --color-rule-border: #564e2c;
        --color-rule-text: #e2cf8c;
        --color-accent-contrast: #0c1512;
        --color-header-bg: #0d0d0b;
        --color-header-text: #f2f1ec;
        --color-header-text-muted: #918e82;

        --decision-pass: #16241f;
        --decision-fail: #2a1b1a;
        --decision-review: #262015;
        --color-pass: #4fbf85;
        --color-fail: #e08079;
        --color-review: #e0b155;

        --focus-ring: #7fa8ff;

        --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 1px rgba(0, 0, 0, 0.3);
    }
}

/* -- Reset / base ------------------------------------------------------ */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    font-size: 100%;
    /* Reserve the scrollbar's width whether or not this page needs one
       (TASK-76). Without it every navigation between a page that scrolls
       and one that does not shifts the whole layout sideways by the
       scrollbar's width -- /status/ scrolls, /jobs/start/ does not, and
       moving between them jumped the header and the page with it.

       Not visible in headless Chromium, which draws overlay scrollbars
       that take no space; it is a desktop-browser fault, which is where
       it was reported from. */
    scrollbar-gutter: stable;
}

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: 1.5;
    background: var(--color-bg);
    color: var(--color-text);
}

h1, h2, h3 {
    line-height: 1.25;
    margin: 0 0 var(--space-3);
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-md); }
h3 { font-size: var(--text-base); font-weight: 600; }

p { margin: 0 0 var(--space-4); }

a {
    color: var(--color-accent);
}

/* One focus style for the whole app (task-7-brief.md guidance: establish
   this now, since the keyboard-driven review screen depends on it). Uses
   :focus-visible so a mouse click doesn't leave a ring behind, but a Tab
   press always does. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: 2px;
}

.skip-link {
    position: absolute;
    left: -999px;
    top: auto;
    background: var(--color-accent);
    color: var(--color-accent-contrast);
    padding: var(--space-2) var(--space-4);
    z-index: 100;
    border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus {
    left: 0;
    top: 0;
}

/* -- Header / nav -------------------------------------------------------- */

.site-header {
    background: var(--color-header-bg);
    color: var(--color-header-text);
    padding: var(--space-3) var(--space-5);
}

.site-header__row {
    max-width: var(--max-width);
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-5);
    flex-wrap: wrap;
}

.site-brand {
    font-size: var(--text-md);
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--color-header-text);
    text-decoration: none;
    margin-right: auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

/* The logo is a square tile; height-matched to the wordmark's line so the
   header row keeps its height (TASK-133). */
.site-brand__logo {
    width: 1.75rem;
    height: 1.75rem;
    display: block;
    flex: none;
}

.site-nav ul {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
}

.site-nav a {
    display: inline-block;
    color: var(--color-header-text-muted);
    text-decoration: none;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius);
    font-size: var(--text-sm);
    font-weight: 600;
}

.site-nav a:hover {
    color: var(--color-header-text);
    background: rgba(255, 255, 255, 0.08);
}

.site-nav a[aria-current="page"] {
    color: var(--color-header-bg);
    background: var(--color-header-text);
}

.site-account {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-sm);
    margin-left: auto;
}

.site-account__who {
    color: var(--color-header-text);
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
}

.site-account__role {
    font-size: var(--text-xs);
    color: var(--color-header-text-muted);
    border: var(--border-width) solid var(--color-header-text-muted);
    border-radius: 999px;
    padding: 0 var(--space-2);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

a.site-account__who {
    text-decoration: underline;
}

/* Which build is running (TASK-138). Chrome, not content: the same muted
   treatment as the role pill it sits beside, so it reads as a footnote to
   the header rather than an eighth navigation item. Tabular figures stop
   the row shifting sideways as the build number gains a digit. The header
   row has ~44px of slack with the admin nav, which is why this shows the
   short form and puts `build 87 · a3f9c21` in the title.

   The negative margin halves the row's 24px gap for this one item, which
   is what buys room for a five-digit build number. It also reads better:
   the build belongs with the account block, not floating apart from it. */
.site-version {
    margin-left: calc(var(--space-3) - var(--space-5));
    font-size: var(--text-xs);
    color: var(--color-header-text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.link-button {
    background: none;
    border: none;
    color: var(--color-header-text-muted);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    padding: 0;
    cursor: pointer;
    text-decoration: underline;
}

.link-button:hover {
    color: var(--color-header-text);
}

/* -- Main layout --------------------------------------------------------- */

.site-main {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: var(--space-6) var(--space-5) var(--space-7);
}

.page-header {
    margin-bottom: var(--space-5);
}

.page-header h1 {
    margin-bottom: 0;
}

.lede {
    font-size: var(--text-md);
    color: var(--color-text-muted);
    max-width: 48rem;
}

/* A clause that explains a word used further down the screen, rather
   than a second sentence of equal weight (TASK-73). */
.lede__aside {
    font-size: var(--text-sm);
}

/* "Exact" stands where a number stands, so it is set like one rather
   than like prose -- a reviewer scanning the column is comparing it
   against 0.83 (TASK-73). */
.queue-card__exact {
    font-weight: 700;
    color: var(--color-text);
}

/* Neither scored nor measured: the promotional check matches words, so
   it has no number and "Exact" against its "I could not tell" would be
   the opposite of true. Set quieter than both, because it is the absence
   of a claim rather than a claim (TASK-73). */
.queue-card__unscored {
    color: var(--color-text-muted);
}

/* -- Components ----------------------------------------------------------- */

.placeholder {
    border: var(--border-width) dashed var(--color-border);
    border-radius: var(--radius);
    padding: var(--space-5);
    color: var(--color-text-muted);
    max-width: 48rem;
}

.placeholder p {
    margin: 0;
}

.card {
    background: var(--color-surface);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card);
    padding: var(--space-5);
    margin-bottom: var(--space-5);
    max-width: 40rem;
}

.card__title {
    margin-bottom: var(--space-1);
}

.card__subtitle {
    color: var(--color-text-muted);
    margin-bottom: var(--space-4);
}

.card__meta {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    margin: var(--space-4) 0 0;
}

/* One row, however many stats there are (TASK-61).
   `repeat(4, ...)` was fine until a fifth arrived and Failed dropped to a
   line of its own. Auto-flow columns take their count from the content,
   so adding or removing a stat needs no modifier class and no second
   place to keep in step. */
.stat-row {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: var(--space-3);
    margin: 0;
}

.stat dt {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
    margin-bottom: var(--space-1);
}

.stat dd {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

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

.job-list__item {
    padding: var(--space-4) 0;
    border-top: var(--border-width) solid var(--color-border);
}

.job-list__item:first-child {
    padding-top: 0;
    border-top: none;
}

.job-list__name {
    font-weight: 600;
    margin: 0 0 var(--space-2);
}

/* Clear of the heading, so the button is not sitting on the title. */
.outputs__get {
    margin: var(--space-4) 0 0;
}

.output-list {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin: var(--space-2) 0 0;
    padding: 0;
}

.output-list a {
    font-size: var(--text-sm);
    font-weight: 600;
}

/* -- Job detail (Task 10) --------------------------------------------------
   Wider than a status card: the failure list carries a post, a reason and
   an action on one line, which doesn't fit the 40rem reading measure the
   status cards use. */

/* One wide, so a card is the same size whichever screen it is on
   (TASK-105). There were three: 40rem plain, 56rem wide, and 72rem
   hand-set on the jobs list and the review queue -- which is the number
   the page container itself uses, so those two were the ones telling the
   truth. The status page's cards were the 40rem odd ones out and read as
   a different app. */
.card--wide {
    max-width: var(--max-width);
}

/* Per-stage progress. Four short bars rather than one long one --
   task-10-brief.md AC1. Every bar carries its count and its percentage as
   text, so the bars are a shape to glance at rather than the only way to
   read the number: nothing here depends on seeing the fill. */

.stage-list {
    list-style: none;
    margin: 0 0 var(--space-5);
    padding: 0;
    display: grid;
    gap: var(--space-3);
}

.stage {
    display: grid;
    grid-template-columns: 1fr minmax(6rem, 12rem) 3rem;
    align-items: center;
    gap: var(--space-3);
}

.stage__head {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
}

.stage__label {
    font-size: var(--text-sm);
    font-weight: 600;
}

.stage__count {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}

.stage__track {
    background: var(--color-bg);
    border: var(--border-width) solid var(--color-border);
    border-radius: 999px;
    height: 0.6rem;
    overflow: hidden;
}

.stage__fill {
    height: 100%;
    background: var(--color-accent);
    border-radius: 999px;
}

.stage__fill--awaiting-review { background: var(--color-review); }
.stage__fill--failed { background: var(--color-fail); }

.stage__percent {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.summary__title {
    margin-bottom: var(--space-3);
}

/* --three and --four are gone: .stat-row counts its own children now.
   Below this width five stats cannot sit side by side without the labels
   colliding, so they wrap into a grid instead -- a wrapped row reads,
   a squeezed one does not. */
@media (max-width: 52rem) {
    .stat-row {
        grid-auto-flow: row;
        grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    }
}

.stat--pass { color: var(--color-pass); }
.stat--review { color: var(--color-review); }
.stat--fail { color: var(--color-fail); }
.stat--error { color: var(--band-errors); }

/* "3 of 4", with the denominator subordinate to the number (TASK-111).
   A bare 3 is not a number anybody can size, and a 3 and a 4 at the same
   weight are two numbers to read rather than one fact. Regular weight,
   muted, and back to lining figures at body size -- tabular-nums on the
   dd keeps the counts in a column, which the trailing phrase would
   otherwise stretch. */
.stat__of {
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--color-text-muted);
    white-space: nowrap;
}

/* Failures. One row per stopped post: what it was, why it stopped, and
   the one action available for it. */

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

/* **Columns, not a block per post** (TASK-129). This was a two-part flex
   row -- everything about the post stacked on the left, the button on
   the right -- so three stopped posts read as three paragraphs and the
   only thing that lined up between them was the button.

   Six tracks, identical on every row and on both cards, so the accounts
   sit in a column and the URLs sit in a column. `minmax(0, ...)` on the
   three that hold text of unpredictable length is what keeps them there:
   without it a yt-dlp message a paragraph long sets its track's width
   from its own content and the row stops matching the row above it.

   Baseline alignment rather than centre, because the error cell can wrap
   to several lines and the first line of it is what the other five cells
   belong beside. */
.failure {
    display: grid;
    grid-template-columns:
        auto            /* platform mark */
        minmax(0, 1fr)  /* account */
        auto            /* content type */
        minmax(0, 1.1fr)/* where the post is */
        minmax(0, 1.7fr)/* what went wrong */
        auto;           /* Retry */
    align-items: baseline;
    gap: var(--space-1) var(--space-3);
    padding: var(--space-3) 0;
    border-top: var(--border-width) solid var(--color-border);
}

.failure:first-child {
    padding-top: 0;
    border-top: none;
}

/* The mark is 16px and aria-hidden; the platform is readable from the
   URL beside it, so this column is recognition rather than information. */
.failure__platform {
    line-height: 1;
}

.failure__account {
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* Same treatment as the results list: an account nobody could identify
   is a fact about the row, not a name, so it is not set like one. */
.failure__account--unknown {
    font-weight: 400;
    color: var(--color-text-muted);
}

.failure__where {
    font-size: var(--text-sm);
    overflow-wrap: anywhere;
}

/* The tool's own words, quoted rather than paraphrased. Monospace
   because it is output, not prose. A block inside the error cell since
   TASK-129, so it wraps in that column rather than across the row. */
.failure__detail {
    display: block;
    margin: var(--space-1) 0 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

.failure__reason {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* Inside the error cell rather than a seventh column (TASK-129): both
   cards share this row shape and only the Errors card has a stage to
   name, so a column of its own would be empty on every Skipped row. */
.failure__stage {
    display: inline-block;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 700;
    color: var(--color-fail);
    margin-right: var(--space-2);
}

.failure__action {
    justify-self: end;
}

/* One button for both cards, sitting directly under the pair it acts on
   (TASK-129). It is outside either card because it means both; the pull
   upwards is what stops it reading as a stray control floating in the
   gap before Outputs. */
.failure__retry-all {
    margin: calc(-1 * var(--space-2)) 0 var(--space-5);
}

.badge {
    display: inline-block;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-radius: 999px;
    padding: var(--space-1) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    color: var(--color-text-muted);
}

.badge--requested {
    border-color: var(--color-review);
    color: var(--color-review);
}

/* -- the whole caption, on hover (TASK-105) ------------------------------- */

/* A caption is cut at 140 so a card is not mostly caption, and it opens
   in place when you hover or focus it.

   **Not a floating bubble** (TASK-110). It was one: absolutely
   positioned, `visibility: hidden` when closed. Both halves of that were
   wrong inside `.review-decide`, which scrolls -- laid out but invisible,
   it gave the panel a horizontal scrollbar for content nobody could see,
   and when shown it was clipped by the same scroll box. Text that grows
   where it stands has neither problem, and needs four rules rather than
   fifteen. */
/* A caption clamped to a number of lines rather than cut at a character
   count (2026-08-20). The review screen's right-hand column is a fixed
   height and must not need a scrollbar before the reviewer has touched
   anything, and two lines is what it can spare.

   Lines, not characters, because how many characters make two lines
   depends on the caption -- a count that guessed would be wrong for every
   caption that is not average. The whole thing stays in the markup and
   opens on hover or focus, so nothing is hidden from a reader who wants
   it; the partial gives this a `tabindex` so that works without a mouse. */
.caption-clamp {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: var(--caption-lines, 2);
    line-clamp: var(--caption-lines, 2);
    overflow: hidden;
}

.caption-clamp:hover,
.caption-clamp:focus-visible {
    -webkit-line-clamp: unset;
    line-clamp: unset;
    overflow: visible;
}

.full-text {
    cursor: help;
    text-decoration: underline dotted var(--color-border);
    text-underline-offset: 3px;
}

.full-text__all { display: none; }

.full-text:hover .full-text__short,
.full-text:focus-visible .full-text__short { display: none; }

.full-text:hover .full-text__all,
.full-text:focus-visible .full-text__all { display: inline; }

/* -- rule and content-type tags (TASK-95) --------------------------------- */

/* Two marks, so a rule name and a content type are recognisable as what
   they are wherever they turn up rather than reading as prose. One
   treatment each, never one per value: the point is to mark the
   category, and a palette of five rule colours would ask a reader to
   learn five things to gain nothing.

   They are told apart by three channels, not just colour: the rule tag
   is squared off, tinted and semibold; the type tag is a plain
   uppercase pill in the muted grey. Either alone survives being read
   in monochrome or by someone who cannot separate the hues. */
.tag {
    display: inline-block;
    /* A tag that is a grid item stretches to its track by default, so
       the rule name on the job screen filled a 14rem column while the
       same tag elsewhere shrank to its words (TASK-101). No effect
       outside a grid. */
    justify-self: start;
    font-size: var(--text-xs);
    padding: 0.05rem var(--space-2);
    border: var(--border-width) solid;
    white-space: nowrap;
    vertical-align: baseline;
}

/* A rule is the thing the app is *about*, so its tag carries the accent
   and a squared corner -- it is not a status, and a pill would put it in
   the same family as the verdict badges, which is exactly what it is
   not. */
.tag--rule {
    border-radius: 3px;
    font-weight: 600;
    border-color: var(--color-rule-border);
    background: var(--color-rule-bg);
    color: var(--color-rule-text);
}

/* A content type is a fact about the post, in the same register as the
   verdict badges beside it. */
.tag--type {
    border-radius: 999px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-color: var(--color-border);
    color: var(--color-text-muted);
}

/* Wrapping is better than clipping in a narrow column. */
.tag--wrap { white-space: normal; overflow-wrap: anywhere; }

/* -- Review queue (Task 11) ------------------------------------------------
   A block of similar work per card: same account, same rule. The facts on
   the right (verdict, confidence, views) are what decides whether to open
   an item; the sentence on the left is what it was flagged for. */

.queue-filter {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
    flex-wrap: wrap;
}

.queue-filter label {
    font-size: var(--text-sm);
    font-weight: 600;
}

.queue-filter select {
    font: inherit;
    padding: var(--space-2) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);
    max-width: 28rem;
}

.queue-count {
    color: var(--color-text-muted);
    margin-bottom: var(--space-4);
}

.queue-group__rule {
    font-weight: 400;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
    margin-left: var(--space-2);
}

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

/* A fixed left column, so Verdict / Confidence / Views land at the same
   x on every row (TASK-50). Flex with space-between put them wherever the
   reason text happened to end, so nothing lined up down the page and the
   eye had to re-find the numbers on each item. Each item is its own grid,
   so the width has to be a real length rather than a fraction -- grid
   tracks only align within one grid. */
.queue-item {
    display: grid;
    grid-template-columns: 34rem 1fr;
    align-items: start;
    gap: var(--space-5);
    padding: var(--space-3) 0;
    border-top: var(--border-width) solid var(--color-border);
}

.queue-item:first-child {
    padding-top: 0;
    border-top: none;
}

.queue-item__what {
    min-width: 0;
}

.queue-item__post {
    margin: 0 0 var(--space-1);
    font-weight: 600;
}

.queue-item__post .card__meta {
    margin: 0 0 0 var(--space-2);
}

.queue-item__why {
    margin: 0;
    font-size: var(--text-sm);
}

.queue-item__facts {
    display: flex;
    gap: var(--space-5);
    margin: 0;
    flex: none;
}

.queue-item__facts dt {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
}

.queue-item__facts dd {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.queue-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
}

/* -- Review screen (Task 12) -----------------------------------------------
   One post per screen, no scrolling to decide (task-12-brief.md AC1): a
   two-column grid sized to what's left of the viewport under the header,
   with the media on the left and everything a decision needs on the
   right. Both columns scroll internally if a post carries unusually long
   evidence, so the *page* never does -- the decision controls stay in the
   same place on every post, which is what makes several hundred decisions
   in a sitting bearable. */

.review-screen {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(22rem, 0.9fr);
    gap: var(--space-5);
    /* Header (~4.5rem) plus the page's own vertical padding. */
    height: calc(100dvh - 12rem);
    min-height: 26rem;
}

.review-media {
    background: var(--color-surface);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card);
    padding: var(--space-5);
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow-y: auto;
}

/* The right-hand column is three cards now, not one panel (TASK-71). It
   keeps the scrolling -- the page itself must never scroll, so that a
   reviewer's controls stay in the same place on every post -- and the
   cards inside it carry the surface. */
.review-decide {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    min-height: 0;
    overflow-y: auto;
}

.review-card {
    background: var(--color-surface);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card);
    padding: var(--space-4) var(--space-5);
}

/* The evidence is what varies in length, so it takes the remaining
   height and scrolls; the other three cards stay their own size. The
   verdict card is last and must never be pushed off the bottom -- it is
   the one being used several hundred times a sitting (TASK-76). */
/* Four labelled rows and nothing else since the evidence moved to its
   own card (TASK-106), so it takes the height it needs. It used to take
   whatever was left and scroll inside itself, which is what a card
   holding the evidence, the frames and the judged panel had to do. */
.review-card--decision {
    flex: 0 0 auto;
}

.review-card--verdict {
    flex: 0 0 auto;
}

/* Which way it went, before a word is read. Only the three a reviewer
   acts on: an errored or unrun check has no verdict to colour, and
   tinting it would say one was reached. */
.decision--pass { background: var(--decision-pass); }
.decision--fail { background: var(--decision-fail); }
.decision--needs_review { background: var(--decision-review); }

.review-card__title {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}

.review-card__lead {
    margin: 0;
    font-weight: 600;
}

.review-card__meta {
    margin: var(--space-1) 0 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* The four questions every rule answers, set apart from the rule's own
   detail below them so a reviewer knows which half of the card will be
   in the same place on the next post (TASK-71). */
.review-standard {
    margin: 0 0 var(--space-4);
    display: grid;
    gap: var(--space-2);
}

.review-standard > div {
    display: grid;
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--space-1) var(--space-3);
    align-items: baseline;
}

/* The same label as `.review-head` and `.review-post` above it: three
   cards in a column, one way of reading a labelled row. */
.review-standard dt {
    color: var(--color-text-muted);
    font-weight: 700;
    font-size: var(--text-sm);
}

.review-standard dd {
    margin: 0;
}

/* No rule above it: inside the evidence card the frames are one more
   thing the card is showing, not a section of their own. */
.review-detail__title {
    margin: var(--space-3) 0 var(--space-2);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}

.review-media__frame {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    gap: var(--space-2);
    background: var(--color-bg);
    border: var(--border-width) dashed var(--color-border);
    border-radius: var(--radius);
    padding: var(--space-5);
}

.review-media__kind {
    margin: 0;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 700;
    color: var(--color-text-muted);
}

.review-media__reference {
    margin: 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--text-sm);
}

.review-media__note {
    margin: 0;
    max-width: 28rem;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

/* The queue-position badge floats over the top right of both columns
   (TASK-72). It is progress through the sitting, not part of the
   decision, and inside the Decision panel it pushed the post's identity
   down the card and read as the first fact about the post.

   Absolute rather than in flow so the two columns keep their full
   height: .review-screen is sized against the viewport so the page never
   scrolls, and a badge in the flow above it would take that height from
   the cards. */
.review-layout {
    position: relative;
}

.result-actions {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-top: var(--space-4);
}

.review-progress {
    position: absolute;
    top: calc(-1 * var(--space-3));
    right: var(--space-3);
    z-index: 2;
    margin: 0;
    padding: var(--space-1) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    box-shadow: var(--shadow-card);
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.review-account {
    margin-bottom: var(--space-1);
}

/* Rule, verdict, confidence -- three labelled rows rather than one run
   of text (TASK-93). The card used to open with the rule as quiet grey
   meta and then "Fail 0.83 confident" on one line, the score hung off
   the verdict word; read out it came to "DECISION Lettermark Fail 0.83
   confident Why No S...", four facts with nothing between them.

   A label column, so each fact is named and the values line up under
   each other -- the same shape as the detail rows below, which is the
   point: the head is the summary of the same kind of thing. */
/* The same grid, labels and sizes as `.review-post` in the card above
   (TASK-101). These two lists sit one under the other in the same
   column, and this one had its own uppercase micro-label and its own
   type scale -- so two adjacent cards asking the reader to learn two
   ways of reading a labelled row.

   9rem and the muted 700 label come from `.review-post`, which shares
   them with `.review-standard` for the same reason. */
.review-head {
    display: grid;
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--space-1) var(--space-3);
    align-items: baseline;
    margin: 0;
    font-size: var(--text-sm);
}

.review-head__row { display: contents; }

.review-head dt {
    color: var(--color-text-muted);
    font-weight: 700;
}

.review-head dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The verdict keeps its colour and its weight and gives up its size.
   Weight as well as colour, so it is still the answer on the card for a
   reader who cannot see the red. */
.review-verdict {
    font-weight: 700;
}

.review-confidence {
    font-variant-numeric: tabular-nums;
}

/* The Job card is one row and has nothing under it, so it wants the
   labelled shape without the rule that separates a head from a body. */
.review-head--plain {
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: 0;
}

/* This post's specifics, behind a press (TASK-103). Closed by default:
   the verdict, the confidence and the why are the decision, and on a
   post where the three agree a reviewer never opens this. */
/* Its own card now, opened by its title (TASK-106). Inside the Decision
   card, opening it grew that card and the decision needed a scrollbar --
   which is the thing it was added to prevent. */
.evidence {
    font-size: var(--text-sm);
}

/* The summary is the card's heading, so it reads like one rather than
   like a link. The marker is what says it opens. */
.evidence > summary {
    cursor: pointer;
    margin: 0;
}

.evidence > summary:hover,
.evidence > summary:focus-visible {
    text-decoration: underline;
}

.evidence[open] > summary {
    margin-bottom: var(--space-3);
}

.evidence .review-standard {
    margin-bottom: 0;
}

.review-card--evidence {
    padding: var(--space-4) var(--space-5);
}

/* A timestamp that jumps the player rather than a timestamp to scrub for
   by hand (TASK-110). It reads as text and behaves as a control, which
   is what it is: the number is the information, the jump is the
   convenience. */
/* A timestamp that jumps the player. The dashed underline read as a
   border round a box, so it is a plain link now -- which is what it
   behaves like. */
.seek {
    font: inherit;
    font-variant-numeric: tabular-nums;
    color: var(--color-accent);
    background: none;
    border: 0;
    padding: 0;
    margin-right: var(--space-2);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

/* A bolded match inside a keyword finding is the words the rule caught,
   not emphasis on a sentence, so it is marked rather than merely bold. */
.evidence b {
    background: var(--color-rule-bg);
    box-shadow: 0 0 0 2px var(--color-rule-bg);
    border-radius: 2px;
}

/* "Not scored" is a fact about the rule, not a missing value, so it is
   worded rather than blank -- but it is not a number, so it does not
   get a number's weight. */
.review-confidence--none {
    color: var(--color-text-muted);
}

/* Scoped to the review screen's headline verdict (TASK-65). Unqualified,
   these also coloured every row of the job screen's results list, which
   carries the same `verdict--*` modifier -- a whole row in red rather
   than a badge. */
.review-verdict.verdict--fail { color: var(--color-fail); }
.review-verdict.verdict--needs_review { color: var(--color-review); }
.review-verdict.verdict--pass { color: var(--color-pass); }

/* The sentence, not the score: doc-3 Architecture, "The verdict explains
   itself in one line". Sized to be the thing the eye lands on. */
.review-why {
    margin: 0 0 var(--space-4);
    font-size: var(--text-md);
}

.review-note-line {
    margin: 0 0 var(--space-4);
    font-size: var(--text-sm);
    color: var(--color-review);
}

.review-evidence {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-1) var(--space-4);
    margin: 0 0 var(--space-5);
    font-size: var(--text-sm);
}

.review-evidence > div {
    display: contents;
}

.review-evidence dt {
    color: var(--color-text-muted);
    text-transform: capitalize;
}

.review-evidence dd {
    margin: 0;
    font-weight: 600;
}

/* Side by side, not stacked (TASK-76). Two full-width boxes cost the
   card most of its height for a choice with two options, and pushed the
   feedback field and the button below the fold on a post with much
   evidence. */
.review-choices {
    border: none;
    margin: 0 0 var(--space-3);
    padding: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
}

.review-choices legend {
    grid-column: 1 / -1;
}

.review-choices legend {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
    padding: 0;
    margin-bottom: var(--space-2);
}

.review-choice {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    /* Tighter than it was: the box is a hit target for a keyboard-first
       screen, so it needs to be comfortably clickable rather than large
       (TASK-76). */
    padding: var(--space-2);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    cursor: pointer;
    font-size: var(--text-sm);
    min-width: 0;
}

.review-choice:has(input:checked) {
    border-color: var(--color-accent);
    box-shadow: inset 0 0 0 1px var(--color-accent);
}

/* The key badge. Same shape everywhere it appears so a reviewer learns
   one vocabulary: a letter in a box means "press this". */
.review-choice__key {
    flex: none;
    display: inline-block;
    min-width: 1.6rem;
    text-align: center;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    border: var(--border-width) solid var(--color-border);
    border-radius: 4px;
    padding: 0 var(--space-1);
    color: var(--color-text-muted);
    background: var(--color-bg);
}

.review-note {
    display: block;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
    margin-bottom: var(--space-1);
}

.review-form input[type="text"] {
    width: 100%;
    font: inherit;
    padding: var(--space-2) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);
    margin-bottom: var(--space-4);
}

.review-actions {
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.button--quiet {
    background: none;
    color: var(--color-text);
    border: var(--border-width) solid var(--color-border);
}

.review-nav {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-top: auto;
    padding-top: var(--space-5);
    font-size: var(--text-sm);
}

/* The link's underline ran across the key badge as well as the word,
   because a text decoration is drawn across an inline-block rather than
   skipping it (TASK-105). Underlining the label alone says the same
   thing without boxing the letter twice. */
.review-nav a,
.review-nav button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    text-decoration: none;
}

.review-nav a[href]:hover .review-nav__label,
.review-nav a[href]:focus-visible .review-nav__label {
    text-decoration: underline;
}

/* An inert nav item -- the key exists, there's just nowhere to go. */
.review-nav a:not([href]) {
    color: var(--color-text-muted);
    cursor: default;
}

.link-button--dark {
    color: var(--color-accent);
}

.link-button:disabled {
    color: var(--color-text-muted);
    cursor: default;
    text-decoration: none;
}

.review-parked {
    margin: var(--space-4) 0 0;
    font-size: var(--text-sm);
    color: var(--color-review);
}

/* -- Reports (Task 13) -----------------------------------------------------
   Server-rendered charts: ranking bars in HTML, one trend line in inline
   SVG. Every chart is a single series, so nothing here uses colour to
   tell things apart -- the fill is one hue, and the label and the value
   sit beside it as text. Marks follow the house spec: bars capped in
   thickness with a rounded data-end and a square baseline, a 2px line
   with round caps, markers at least 8px across with a surface ring, and
   recessive rules. Light and dark come from the tokens at the top of this
   file, so there is one stylesheet rather than a theme fork. */

.hero {
    display: flex;
    align-items: baseline;
    gap: var(--space-4);
    margin: 0 0 var(--space-3);
    flex-wrap: wrap;
}

.hero__value {
    font-size: 3rem;
    line-height: 1;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--color-pass);
}

.hero__detail {
    color: var(--color-text-muted);
}

/* A single ratio against its whole -- one fill on a neutral track, not
   two categories sharing a bar. */
.meter {
    height: 0.75rem;
    border-radius: 999px;
    background: var(--color-bg);
    border: var(--border-width) solid var(--color-border);
    overflow: hidden;
}

.meter__fill {
    height: 100%;
    background: var(--color-pass);
    border-radius: 999px 0 0 999px;
}

.bars {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-3);
}

.bar {
    display: grid;
    grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr) minmax(9rem, auto);
    align-items: center;
    gap: var(--space-4);
}

.bar--empty {
    display: block;
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.bar__label {
    font-size: var(--text-sm);
    font-weight: 600;
    overflow-wrap: anywhere;
}

.bar__track {
    display: block;
    height: 1rem;
    background: var(--color-bg);
    border-radius: 2px;
}

.bar__fill {
    display: block;
    height: 100%;
    background: var(--color-accent);
    /* Square where it grows from the baseline, rounded at the data end. */
    border-radius: 0 4px 4px 0;
    min-width: 2px;
}

.bar__value {
    font-size: var(--text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.bar__of {
    display: inline-block;
    margin-left: var(--space-2);
    font-weight: 400;
    color: var(--color-text-muted);
}

/* What tells two accounts sharing a display name apart (TASK-36). Set
   below the name rather than beside it: it answers "which of the two is
   this", so it is secondary to the name, and inline it would push the
   label column wide enough to squeeze the bars it sits next to. */
.bar__qualifier {
    display: block;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--color-text-muted);
}

/* The box matches the viewBox's proportions (core/views.py,
   TREND_VIEWBOX) so the chart fills the card's width instead of
   letterboxing inside it -- it keeps its aspect ratio rather than
   stretching, because a stretched circle is an ellipse. */
/* `aspect-ratio` is set inline from core.views.TREND_ASPECT (TASK-92).
   It was a literal here, 236 / 56, left behind by an earlier pair of
   margins -- so the box and the viewBox could disagree, and did. */
.trend {
    display: block;
    width: 100%;
    margin-bottom: var(--space-5);
}

.trend__line {
    fill: none;
    stroke: var(--color-accent);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
    /* User space is 100x40 but the box is much wider on screen, so the
       stroke would otherwise be drawn thicker horizontally than
       vertically. */
    vector-effect: non-scaling-stroke;
}

.trend__dot {
    fill: var(--color-accent);
    stroke: var(--color-surface);
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
}

.trend__label {
    fill: var(--color-text);
    font-size: 5px;
    font-weight: 700;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
}

.data-table th,
.data-table td {
    text-align: left;
    padding: var(--space-2) var(--space-3) var(--space-2) 0;
    border-bottom: var(--border-width) solid var(--color-border);
}

.data-table thead th {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
}

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

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* -- Jobs list (TASK-17) --------------------------------------------------- */

.status-filter {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-5);
}

.status-filter a {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-2);
    font-size: var(--text-sm);
    font-weight: 600;
    text-decoration: none;
    padding: var(--space-2) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: 999px;
    color: var(--color-text);
}

.status-filter a[aria-current="true"] {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-contrast);
}

.status-filter__count {
    font-variant-numeric: tabular-nums;
    color: var(--color-text-muted);
}

.status-filter a[aria-current="true"] .status-filter__count {
    color: inherit;
}

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

/* Width comes from `.card--wide` now (TASK-105), which is this number:
   both are a list of boxes you scan down, and different margins on the
   two screens read as two different apps rather than two views of one. */

.job-card__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    /* Clear of the progress bar underneath, which was sitting right
       against the job's name. */
    margin-bottom: var(--space-4);
}

.job-card .card__title {
    margin-bottom: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The one thing the subtitle carried that the name does not, so it moved
   up here (TASK-96). Tabular and muted: it is how you refer to a job out
   loud, not what you read the card for. */
.job-card__number {
    font-variant-numeric: tabular-nums;
    color: var(--color-text-muted);
    font-weight: 400;
}

/* Status badges reuse the verdict tokens: a failed job is the same red as
   a failed check, and an awaiting-review job the same amber as one -- the
   colour means the same thing in both places. The label is always there,
   so the colour is reinforcement rather than the message. */
.badge--running { border-color: var(--color-accent); color: var(--color-accent); }
.badge--complete { border-color: var(--color-pass); color: var(--color-pass); }
.badge--failed { border-color: var(--color-fail); color: var(--color-fail); }
.badge--awaiting_review { border-color: var(--color-review); color: var(--color-review); }

.review-count {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    margin: 0 0 var(--space-4);
}

.review-count__number {
    font-size: var(--text-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--color-accent);
}

/* -- Forms ------------------------------------------------------------- */

.form {
    max-width: 32rem;
}

.form--narrow {
    max-width: 22rem;
}

.field {
    margin-bottom: var(--space-4);
}

.field label {
    display: block;
    font-size: var(--text-sm);
    font-weight: 600;
    margin-bottom: var(--space-1);
}

.field input,
.field select,
.field textarea {
    width: 100%;
    font: inherit;
    padding: var(--space-2) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);
}

.form-error {
    color: var(--color-fail);
    font-size: var(--text-sm);
    margin-bottom: var(--space-4);
}

/* form-error doubles as a <ul> when a field carries more than one error
   (StartJobForm.clean_pasted_urls -- task-9-brief.md AC3, "reported by
   line, with the offending text": each bad line is its own list item) --
   this only adds the list affordances the plain-paragraph case doesn't
   need. */
ul.form-error {
    padding-left: var(--space-5);
}

ul.form-error li {
    margin-bottom: var(--space-1);
}

.hint {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    margin: var(--space-1) 0 0;
}

button,
.button {
    font: inherit;
    font-weight: 600;
    font-size: var(--text-sm);
    background: var(--color-accent);
    color: var(--color-accent-contrast);
    border: none;
    border-radius: var(--radius);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
}

button:hover,
.button:hover {
    filter: brightness(1.08);
}

/* -- Responsive ----------------------------------------------------------
   Laptop (~1280px+) is the primary target; nothing above should break
   between that and tablet width. The nav wraps onto its own line rather
   than collapsing into a hidden menu -- no JS, and every destination
   stays visible and reachable at any width. */

@media (max-width: 900px) {
    .site-header__row {
        gap: var(--space-3);
    }

    .site-nav {
        order: 3;
        width: 100%;
    }

    .site-nav ul {
        justify-content: flex-start;
    }

    .stat-row {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .stage {
        grid-template-columns: 1fr 4rem;
        row-gap: var(--space-1);
    }

    .stage__track {
        grid-column: 1 / -1;
        order: 3;
    }

    .queue-item {
        flex-direction: column;
        gap: var(--space-2);
    }

    /* The six columns collapse to three (TASK-129). Six tracks in the
       width left below 900px gives the error column about four
       characters, so the two cells that hold sentences take the full
       width and the row becomes four short lines instead. `.failure` is
       a grid now, so the `flex-direction: column` it used to share with
       `.queue-item` here did nothing. */
    .failure {
        grid-template-columns: auto minmax(0, 1fr) auto;
        gap: var(--space-1) var(--space-2);
    }

    .failure__where,
    .failure__reason,
    .failure__action {
        grid-column: 1 / -1;
    }

    .failure__action {
        justify-self: start;
    }

    .queue-item__facts {
        gap: var(--space-4);
    }

    /* Below tablet width the two review columns stack, and the screen is
       allowed to scroll: "no scrolling to decide" is a promise about the
       laptop this is worked on, not about a phone nobody reviews 350
       posts from. */
    .review-screen {
        grid-template-columns: 1fr;
        height: auto;
    }

    /* The bar label goes above its own bar rather than shrinking to a
       column too narrow to read an account name in. */
    .bar {
        grid-template-columns: 1fr auto;
        gap: var(--space-1) var(--space-3);
    }

    .bar__track {
        grid-column: 1 / -1;
        order: 3;
    }
}

@media (max-width: 480px) {
    .stat-row {
        grid-template-columns: 1fr;
    }
}

/* --- What a job's checks found (TASK-46) --------------------------------
   The job screen showed progress and failures and never a verdict, so the
   paste-a-URL flow ended on a page that never said what the app decided. */

/* Title and subtitle on the left, how many posts on the right. The count
   used to sit on a line of its own between the filter and the list,
   where it read as a third control rather than as the answer to "how
   big is this". */
.results__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3) var(--space-5);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}

.results__heading .card__subtitle {
    margin-bottom: 0;
}

.results__tally {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The controls, as one recessed strip. They were three loose widgets on
   the card's own background, so the eye had to work out that the two
   selects and the button belonged together. */
.results-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
    margin: 0 0 var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--color-bg);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    font-size: var(--text-sm);
}

.results-filter label {
    color: var(--color-text-muted);
    font-weight: 600;
}

/* The results filter never got the styling the review queue's filter
   has, so these were raw browser selects sitting next to a styled
   button.

   `appearance: none` rather than just a background colour: with the
   control left at `auto`, Chromium paints its own light chrome over the
   author background, so a select that computes to #1f1f1b still renders
   white on the dark theme. The arrow is two gradients rather than an
   SVG data URI -- the app runs under a strict CSP and this needs no
   opinion from it. */
.results-filter select {
    appearance: none;
    font: inherit;
    font-size: var(--text-sm);
    padding: var(--space-1) var(--space-6) var(--space-1) var(--space-2);
    border: var(--border-width) solid var(--color-border);
    border-radius: 6px;
    background-color: var(--color-surface);
    color: var(--color-text);
    max-width: 16rem;
    background-image:
        linear-gradient(45deg, transparent 50%, currentColor 50%),
        linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position:
        calc(100% - 1rem) calc(50% + 1px),
        calc(100% - 0.7rem) calc(50% + 1px);
    background-size: 5px 5px;
    background-repeat: no-repeat;
}

/* Pushed to the far end: Clear undoes the row rather than continuing
   it, so it should not sit in the run of controls that build a filter. */
.results-filter__clear {
    margin-left: auto;
    font-weight: 600;
}

.results {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-4);
}

/* A result is a box, not a rule-separated row. On a page of 25 the
   hairline borders left it ambiguous which verdicts belonged to which
   post -- the gap between two results looked the same as the gap
   between a post and its own checks. */
.result {
    /* A stronger edge than the app's default hairline (TASK-70). These
       boxes contain their own internal rules -- one per verdict row -- so
       at the same weight as those the box stopped reading as a box and a
       page of results became one undifferentiated grid. The outline needs
       to beat what it holds. */
    border: 2px solid var(--color-result-border);
    border-radius: var(--radius);
    background: var(--color-bg);
    /* The verdict rows carry a coloured left edge that would otherwise
       square off the bottom corners. */
    overflow: hidden;
}

/* The post's own answer: a tinted header and a badge saying what it adds
   up to (2026-08-20). The page is read to find which posts are a problem,
   and answering that meant reading every verdict row on every card.

   `color-mix` against the card's own ground rather than fixed pale hexes,
   so the tints follow --color-pass/--color-fail into dark mode instead of
   staying pastel on a dark card. 12% is enough to read as a state at a
   glance and light enough to leave the text on it at full contrast.

   Three states. "Awaiting review" is untinted on purpose: it is the
   absence of an answer, and giving it a colour of its own would make a
   page of undecided posts look decided. */
.result--compliant .result__post {
    background: color-mix(in srgb, var(--color-pass) 12%, var(--color-surface));
}

.result--breach .result__post {
    background: color-mix(in srgb, var(--color-fail) 12%, var(--color-surface));
}

/* Pushed to the end of the identifying line, so it lands in the same place
   on every card and the eye can run down it. */
.result__compliance {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-left: auto;
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--color-text-muted);
}

.result--compliant .result__compliance { color: var(--color-pass); }
.result--breach .result__compliance { color: var(--color-fail); }

.result__post {
    display: grid;
    gap: var(--space-1);
    padding: var(--space-4);
}

/* Who posted it, what kind it is, how many saw it -- the line that
   identifies the post while scanning. */
.result__account {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    min-width: 0;
    font-size: var(--text-md);
    font-weight: 600;
    line-height: 1.3;
}

.result__account .platform-icon {
    width: 20px;
    height: 20px;
    flex: none;
    margin-right: 0;
    vertical-align: baseline;
}

.result__name {
    min-width: 0;
    overflow-wrap: anywhere;
}

.result__name--unknown {
    color: var(--color-text-muted);
    font-weight: 400;
}

/* Look comes from `.tag--type` now (TASK-95); this only positions it. */
.result__type {
    white-space: nowrap;
}

.result__views {
    margin-left: auto;
    flex: none;
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}

.result__what {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

/* Long, and not being read -- it is there to be clicked and to confirm
   which post this is. Same treatment as the queue card's URL. */
.result__where {
    margin: 0;
    font-size: var(--text-sm);
    min-width: 0;
}

.result__where a {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

/* The decisions, on the raised surface so they read as the content of
   the box rather than more of its header. */
.verdicts {
    list-style: none;
    margin: 0;
    padding: 0;
    background: var(--color-surface);
    border-top: var(--border-width) solid var(--color-border);
}

/* Badge, rule, reason -- full width, because the reason is a sentence.
   Boxed into the old 26rem side column it had about 5rem to wrap in and
   came out as a vertical ribbon two words wide; here it gets whatever
   the card has left, which is around 30rem. */
.verdict {
    font-size: var(--text-sm);
    border-top: var(--border-width) solid var(--color-border);
    /* No colour spine (TASK-76). It carried the same fact as the badge
       beside it, and a column of colour down the left of a 25-row list
       competed with the badges for the reader's eye rather than helping
       them. The badge still carries the verdict in colour and in words. */
}

/* The row itself is the link (TASK-74): a page of results is 25 rows,
   and a trailing "open" on each would be 25 more tab stops to reach the
   one you want. The grid moved here with it, so the spine and the
   dividers stay on the <li> and the hit area is the whole row. */
.verdict__open {
    display: grid;
    /* 8.5rem is what "Needs review" and "Not checked" actually measure at
       text-xs uppercase with the tracking on -- at 7rem they overflowed
       the track and butted straight into the rule name. */
    grid-template-columns: 8.5rem minmax(0, 14rem) minmax(0, 1fr);
    gap: var(--space-2) var(--space-4);
    align-items: baseline;
    padding: var(--space-3) var(--space-4);
    color: inherit;
    text-decoration: none;
}

.verdict__open:hover,
.verdict__open:focus-visible {
    background: var(--color-bg);
}

/* The rule name is what a reader is scanning for, so that is what
   underlines rather than the whole row going blue. */
.verdict__open:hover .verdict__rule,
.verdict__open:focus-visible .verdict__rule {
    text-decoration: underline;
}

.verdict:first-child {
    border-top: 0;
}


/* Neutral until a verdict says otherwise, so an unrecognised one is
   legible rather than invisible. */
.verdict__badge {
    justify-self: start;
    padding: 0.05rem 0.4rem;
    border-radius: 4px;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
    background: var(--color-bg);
    border: var(--border-width) solid var(--color-border);
    color: var(--color-text-muted);
}

/* Tinted ground with the verdict colour as the text, rather than white
   on a solid fill. The solid version hard-coded `#fff`, which is fine
   against the light theme's #1f7a4d and close to unreadable against the
   dark theme's #4fbf85 -- and the dark palette exists precisely because
   these badges have to hold up on a dark surface. */
.verdict--pass .verdict__badge {
    background: color-mix(in srgb, var(--color-pass) 14%, var(--color-surface));
    border-color: color-mix(in srgb, var(--color-pass) 40%, var(--color-surface));
    color: var(--color-pass);
}

.verdict--fail .verdict__badge {
    background: color-mix(in srgb, var(--color-fail) 14%, var(--color-surface));
    border-color: color-mix(in srgb, var(--color-fail) 40%, var(--color-surface));
    color: var(--color-fail);
}

.verdict--needs_review .verdict__badge {
    background: color-mix(in srgb, var(--color-review) 14%, var(--color-surface));
    border-color: color-mix(in srgb, var(--color-review) 40%, var(--color-surface));
    color: var(--color-review);
}

.verdict__rule {
    font-weight: 600;
    overflow-wrap: anywhere;
}

.verdict__why {
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

/* Below this the three tracks stop being readable, so the reason takes
   a line of its own under the badge and the rule rather than squeezing
   them -- it is the longest and the least useful at a glance. */
@media (max-width: 48rem) {
    .verdict {
        grid-template-columns: 8.5rem minmax(0, 1fr);
        row-gap: var(--space-1);
    }

    .verdict__why {
        grid-column: 1 / -1;
    }
}

/* The post itself, on the screen where somebody judges it (TASK-47).
   Capped by height rather than width: these are 9:16 reels, and letting
   one run to its natural height would push the decision controls off the
   fold -- which is the thing TASK-12 set out to avoid. */
.review-media__player {
    display: block;
    max-width: 100%;
    max-height: 60vh;
    width: auto;
    height: auto;
    margin: 0 auto;
    border-radius: 4px;
    background: var(--color-bg);
}

.review-account--unknown {
    color: var(--color-text-muted);
    font-weight: 400;
}

/* The post identified on the decision screen the way a queue card
   identifies it (TASK-70). Same label column and same weights as
   .queue-card__fields, so a reviewer arriving from a card is reading the
   same shape rather than re-learning one. */
.review-account {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

.review-account .platform-icon {
    width: 20px;
    height: 20px;
    flex: none;
    margin-right: 0;
}

.review-post {
    display: grid;
    /* The same label column as `.review-standard` below it, so the two
       lists in the right-hand column line up rather than each choosing
       their own indent (TASK-71). */
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--space-1) var(--space-3);
    margin: 0;
    font-size: var(--text-sm);
    align-items: baseline;
}

/* The account line is a heading inside the queue card, where it is the
   card's title. In the Post card it is one field among four, so it is
   set at the size of the rest of them. */
.review-post .queue-card__account {
    font-size: var(--text-sm);
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.review-post dt {
    color: var(--color-text-muted);
    font-weight: 700;
}

.review-post dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* Rules that never ran (TASK-48). Kept out of the review queue so they
   don't bury real decisions, but stated where a reviewer will see them --
   a check that silently doesn't run is doc-1's first-named failure. */
.coverage__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }

.coverage__row {
    display: grid;
    grid-template-columns: minmax(0, 14rem) minmax(0, 12rem) minmax(0, 1fr);
    gap: var(--space-2);
    align-items: baseline;
    font-size: var(--text-sm);
}

@media (max-width: 40rem) {
    .coverage__row { grid-template-columns: 1fr; }
}

.coverage__rule { font-weight: 600; }
.coverage__count { font-variant-numeric: tabular-nums; }
.coverage__why { color: var(--color-text-muted); }


/* The standard five lines of a queue item (TASK-50): which post, whose it
   is and what kind, which check, and why. */
.queue-item__who {
    margin: 0 0 var(--space-1);
    font-size: var(--text-sm);
}

.queue-item__check {
    margin: 0 0 var(--space-1);
    font-size: var(--text-sm);
    font-weight: 600;
}

.queue-item__open a { font-weight: 600; }

/* Below the fixed column width there is no room for two columns at all,
   so the facts stack under the reason rather than being squeezed. */
@media (max-width: 60rem) {
    .queue-item {
        grid-template-columns: minmax(0, 1fr);
    }
}




/* The queue card (TASK-52): labelled rows on the left, the numbers and
   the way in on the right, vertically centred against them. */
/* A bigger card, and a fixed right-hand column (TASK-53).

   `max-content` sized that column to whatever each card's numbers
   happened to be, so "1,956 views" and "-" produced different widths and
   nothing lined up down the page. Every card is its own grid and grid
   tracks only align within one grid, so the only thing that makes them
   agree is an identical fixed length on each. Same reasoning as the
   34rem column in the review list. */
/* **The post on top, the decision underneath** (TASK-128). The two were
   side by side: five labelled rows on the left, the numbers in the 28rem
   column on the right. That described the same post in a different shape
   from the job results list, where the post identifies itself in a header
   and its verdicts sit on a tinted band beneath -- and a reviewer moving
   between the two screens had to re-find the same five facts.

   So the card is a `.result` now: `.queue-card__fields` is the header,
   spanning both columns, and the band under it holds the rule, the
   verdict, the confidence and the way in.

   The 28rem column is still here and still doing its old job: the band's
   numbers sit in it, so Verdict starts at the same x on every card down
   the page. There is no gap between the rows because the band's two
   lines have to meet -- a gap would break its tint into two strips. */
.queue-card__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 28rem;
    gap: 0;
    align-items: start;
    /* The band's ground.

       Flipped 2026-08-20 on request: the decision band is the plain half
       now and the post's own details are the tinted one, so the eye lands
       on what was decided rather than on what it was decided about. */
    background: var(--color-surface);
}

/* The post, treated as the results list treats it (TASK-128): platform
   icon and account leading, the type tag beside them and the view count
   at the far end of that line, then what the post says, then where it is.

   Flex rather than the old two-column grid, because "labels down the
   left, values to their right" is the shape that was being replaced --
   Tim asked for labels and values to be allowed to run across, and this
   line is where that buys the most: three facts on one line instead of
   three rows. `order` puts the values in the results list's order without
   moving anything in `_post_identity.html`, which the review screen
   renders from the same file and in the same order (TASK-71). */
.queue-card__fields {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    grid-column: 1 / -1;
    margin: 0;
    /* No bottom padding: the rule row below carries the space as its own
       margin, so the header's ground stops exactly where the band starts. */
    /* Bottom padding of its own now: the rule row below is a sibling in
       the grid rather than a zero-height row reaching back up into this
       one, so this half has to close itself off. */
    padding: var(--space-5) var(--space-6);
    /* The card's own ground, same as the band below it. The two halves are
       told apart by the rule between them, not by a change of colour --
       tried tinted both ways on request and the border alone reads better.
       Keep the border on `.queue-card__rulerow` and `.queue-card__facts`
       if this ever changes back: it is the only thing separating them. */
    background: var(--color-surface);
    font-size: var(--text-sm);
    min-width: 0;
}

/* Bold (TASK-70). At 400 against a 400 value the label and the thing it
   labels carried the same weight, so the eye had nothing to run down the
   left edge of the card. Still muted, so the values stay the content. */
/* Read, not seen (TASK-128). The results list shows none of these words:
   an icon and a name is an account, a link is a link, a tag is a type,
   and the line under them is what the post says. On a card that is
   scanned 350 times in a sitting they were five words repeated 350 times.

   They stay in the DOM rather than being deleted, because a screen reader
   arriving at "Galaxy AI. Ask it anything." has none of the visual cues
   that make the label unnecessary -- and because the review screen
   renders the same partial with its labels showing. Views is the
   exception: a bare number needs its word, so that one label runs across
   after its value ("49,233 views") rather than above or beside it.

   `:has()` is how the Views label is told from the rest without giving
   the labels classes -- the queue's field order is asserted in
   core/tests/test_review_standardisation.py by matching bare `<dt>` tags,
   so a class on one of them would read as the row having moved. A browser
   too old for `:has()` drops the rule and shows every label, which is the
   card as it was before this change rather than a broken one. */
.queue-card__fields dt:not(:has(+ .queue-card__views)) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.queue-card__fields dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* Account, type, views -- one line, in that order, however they sit in
   the partial's source. */
.queue-card__fields .queue-card__account { order: 1; }
.queue-card__fields .queue-card__type { order: 2; white-space: nowrap; }

/* The type tag's text centred on the account name beside it, rather than
   left wherever the tag's own padding and line-height happen to put it.
   `inline-flex` centres the label inside the tag; the shared line-height
   is what keeps the two texts on one axis whatever either of them says. */
.queue-card__fields .queue-card__type .tag {
    display: inline-flex;
    align-items: center;
    line-height: 1.3;
}
.queue-card__fields .queue-card__views { order: 3; }
.queue-card__fields dt:has(+ .queue-card__views) { order: 4; }
.queue-card__fields .queue-card__what { order: 5; }
.queue-card__fields .queue-card__url { order: 6; }

/* The account leads the line, so it gets the width and the icon sits
   against the name rather than a text margin away from it. `nowrap`
   keeps the two together on a narrow card: wrapping put the icon alone
   on one line and the name on the next, where the whole point of the
   icon is that it is read with the name. */
.queue-card__fields .queue-card__account {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--space-2);
    min-width: 0;
    line-height: 1.3;
}

.queue-card__fields .queue-card__account .platform-icon {
    width: 20px;
    height: 20px;
    flex: none;
    margin-right: 0;
    vertical-align: baseline;
}

/* An account nobody has matched yet is a fact about the post, not the
   post's name, so it does not get the name's weight -- same treatment as
   `.result__name--unknown`. */
.queue-card__fields .queue-card__account--unknown {
    color: var(--color-text-muted);
    font-weight: 400;
}

/* Pushed to the far end of the account line, where `.result__views` was
   always meant to sit. Its label follows it, lowercased, so the pair
   reads as one phrase. */
.queue-card__fields .queue-card__views {
    margin-left: auto;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}

.queue-card__fields dt:has(+ .queue-card__views) {
    /* Closer than the line's own gap: this is a number and its unit, not
       two facts side by side. */
    margin-left: calc(var(--space-1) - var(--space-2));
    color: var(--color-text-muted);
    font-weight: 400;
    text-transform: lowercase;
}

/* No count, no row (TASK-128). The partial still says "Unknown" in
   words, which is right on the review screen where the fact is being
   weighed; on a card being scanned it is a labelled row saying nothing,
   and the results list simply leaves out what it does not have. */
.queue-card__fields .queue-card__views--unknown,
.queue-card__fields dt:has(+ .queue-card__views--unknown) {
    display: none;
}

/* What the post says, and where it is: a line each, under the account,
   muted and small -- `.result__what` and `.result__where`. */
.queue-card__fields .queue-card__what,
.queue-card__fields .queue-card__url {
    flex-basis: 100%;
}

.queue-card__fields .queue-card__what {
    color: var(--color-text-muted);
}

/* The decision band's left half: the rule this card is asking about.

   It used to be the last row of `.queue-card__fields` -- a zero-height
   flex item with a negative margin, painting the band while taking no
   vertical space, so its tag overflowed downwards alongside the numbers
   in the next grid row. That worked visually but put the two in different
   grid rows, so they could not be centred against each other: measured
   2026-08-20, the rule tag sat 22px above the verdict's centre.

   Now it is its own element in grid column 1, opposite `.queue-card__facts`
   in column 2, sharing a top border and vertical padding. One row, two
   halves, centred on the same axis, and no negative margins. */
.queue-card__rulerow {
    grid-column: 1;
    /* `align-self: stretch` beats the body's `align-items: start` for this
       one item: top-aligned, the two halves of the band still could not
       centre against each other, because the numbers opposite are two
       lines tall and this is one. Stretching both to the row's height is
       what gives `align-items: center` below something to centre within. */
    align-self: stretch;
    display: flex;
    align-items: center;
    margin: 0;
    padding: var(--space-4) var(--space-6);
    border-top: var(--border-width) solid var(--color-border);
}

/* Hidden on this screen only, like the labels above it -- the tag says
   what it is. The review screen renders the same pair with it showing. */
.queue-card__rulerow dt {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.queue-card__rulerow dd {
    margin: 0;
    min-width: 0;
}

.queue-card__account { font-weight: 600; }

/* The icons carry their own brand colours now (TASK-62), so `color` no
   longer applies -- only the unknown-platform fallback still inherits
   it, which is the one case where a neutral outline is right. */
.platform-icon {
    vertical-align: -0.15em;
    margin-right: 0.15rem;
    color: var(--color-text-muted);
    border-radius: 3px;
}

/* A full post URL is long and is not the thing being read -- it is there
   to be clicked and to confirm which post this is. */
.queue-card__url a {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

/* Fixed tracks, not flex: flex packs each item to its own content, so
   Confidence started at a different x on a card whose verdict read
   "Needs review" than on one reading "Fail". These four widths are the
   same on every card, so the columns line up down the page. */
/* Even columns, and clear of the fields beside them (TASK-62).
   Verdict had 7rem to Confidence's and Views' 6rem, so the gap after it
   read wider than the gaps between the rest -- a difference of a
   millimetre that the eye picks up as a grouping that is not there.
   Equal tracks, and the block starts further right so the numbers read
   as a summary of the card rather than a fifth labelled row of it. */
/* The band's second line now (TASK-128), in the body's fixed 28rem
   column: the tracks and the offset are what make Verdict start at the
   same x on every card down the page, which is the whole of TASK-53. The
   tint and the padding are what make it, with the rule row above it, one
   area of the card rather than two. */
.queue-card__facts {
    display: grid;
    grid-template-columns: repeat(2, 6.5rem) auto;
    align-items: center;
    gap: var(--space-3);
    margin: 0;
    grid-column: 2;
    /* The band's other half: the same top border and vertical padding as
       `.queue-card__rulerow`, so the dividing line runs unbroken across the
       card and both halves centre on one axis. */
    align-self: stretch;
    border-top: var(--border-width) solid var(--color-border);
    padding-top: var(--space-4);
    padding-right: var(--space-6);
    padding-bottom: var(--space-4);
    /* Further right again (TASK-70). At space-5 the numbers still read as
       a fifth column of the labelled fields rather than as a summary set
       apart from them. */
    padding-left: var(--space-7);
}

/* The way in, at the card's right edge rather than tight against the
   confidence: the third track takes whatever the column has left, and a
   button floating in the middle of it reads as unfinished. */
.queue-card__facts > div:last-child {
    justify-self: end;
}

.queue-card__facts dt {
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    margin-bottom: 0.15rem;
}

.queue-card__facts dd {
    margin: 0;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.button--small {
    padding: 0.3rem 0.75rem;
    font-size: var(--text-sm);
}

/* Below this the two columns cannot both fit, so the facts drop under
   the fields rather than being crushed. */
/* Since TASK-128 they are already stacked -- the header spans and the
   band sits under it -- so what these two queries do now is stop the
   band's numbers hanging off the right in a column there is no room
   for, and pull them back to the card's own padding. */
@media (max-width: 60rem) {
    .queue-card__body { grid-template-columns: minmax(0, 1fr); }
    .queue-card__facts { justify-content: flex-start; }
}


/* Bigger boxes (TASK-53). The queue is the screen doc-3 says the working
   time goes into, and these were the same size as a status-page card
   holding two numbers. */
/* Width comes from `.card--wide` now (TASK-105). It was hand-set here
   because `.card--wide` used to be 56rem, and with a fixed 28rem of
   facts on the right that left roughly 22rem for a caption and a full
   post URL -- not enough to read either. `.card--wide` is that number
   itself now. */
/* The padding moved inside (TASK-128). The decision band has to reach
   the card's edges to read as its own area, so the card holds no padding
   of its own and the header and the band each carry theirs. `overflow:
   hidden` keeps the band's bottom corners inside the card's radius. */
.queue-card {
    padding: 0;
    overflow: hidden;
    margin-bottom: var(--space-5);
}

/* The account is what identifies the post at a glance while scanning the
   page, so it leads at a size that can be read without stopping. */
/* text-md, which is what the job results list leads with (TASK-128). At
   text-lg the account was the biggest thing on the screen after the page
   title, and the same account name is repeated down a queue that is
   grouped by account -- the size was being spent on the fact that
   changes least. */
.queue-card__account {
    font-size: var(--text-md);
    font-weight: 600;
    line-height: 1.25;
}

.queue-card__account .platform-icon {
    width: 22px;
    height: 22px;
    vertical-align: -0.2em;
    margin-right: 0.3rem;
}

@media (max-width: 70rem) {
    .queue-card__body { grid-template-columns: minmax(0, 1fr); }
    .queue-card__facts {
        grid-column: 1 / -1;
        justify-content: start;
        padding-top: var(--space-3);
        padding-left: var(--space-6);
    }

    /* The band stacks here, so the rule row takes the full width and the
       numbers below it drop their own top border -- one line between the
       post and the band, not one between every stacked half. */
    .queue-card__rulerow {
        grid-column: 1 / -1;
        padding-bottom: var(--space-2);
    }

    .queue-card__facts {
        border-top: 0;
        padding-top: 0;
    }
}

/* "4 checks across them" under the awaiting-review count (TASK-54): the
   counters are posts and the review queue lists checks, and one post can
   fail three rules. */
.stat__note {
    margin: var(--space-1) 0 0;
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--color-text-muted);
}


/* The four summary stats, centred in equal shares of the card (TASK-62).
   They were left-aligned inside auto-flowed columns, so each number sat
   at a different distance from its own label and the row read as four
   things placed rather than one row measured.

   This is deliberately the same data as the progress list above it, seen
   the other way round: that one is how far the job got, this is what it
   came to. Keeping both means the second has to look like a summary, not
   like a repeat. */
.stat-row--summary .stat {
    text-align: center;
}


/* The job's own facts, as labelled rows (TASK-63). Same shape as a
   queue card's fields, so a reader moving between the two screens is not
   re-learning where to look. */
/* Same *styling* too, not just the same shape (TASK-131). Two cards sat
   on one screen carrying the same kind of content -- a labelled list of
   facts -- at two sizes and two label weights: text-sm with a 400 label
   here, text-base with a 700 label on a review card. The gaps already
   agreed; type and weight now do as well.

   The label column is 8rem rather than the review card's 6.5rem because
   the labels differ: "Last changed" measures 121px bold at text-base, so
   at the old 7rem it wrapped to two lines the moment the type grew. That
   is fit, not a second opinion about spacing. */
.job-facts {
    display: grid;
    grid-template-columns: 8rem minmax(0, 1fr);
    gap: var(--space-2) var(--space-4);
    margin: 0;
    font-size: var(--text-base);
    align-items: baseline;
}

/* Bold and muted, the review card's treatment (TASK-70, borrowed here in
   TASK-131): at 400 against a 400 value the label and the thing it
   labels carry the same weight and the eye has nothing to run down the
   left edge of the card. */
.job-facts dt {
    color: var(--color-text-muted);
    font-weight: 700;
}

.job-facts dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* Room under the Progress heading (TASK-131). `.card__title` leaves
   space-1, which is right for a heading followed by prose -- but what
   follows this one is a bar, and a 4px gap read as the heading being
   part of it. Scoped to this card rather than raised on `.card__title`
   itself: every other card on the app is the prose case, and the review
   queue's spacing is not to move. */
#job-progress > .card__title {
    margin-bottom: var(--space-3);
}

/* Paging, as a footer to the list. Previous and Next keep their box when
   there is nowhere to go rather than disappearing, so the control that
   is still live does not move between pages -- the same reasoning as the
   review screen's inert nav items. */
.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: var(--border-width) solid var(--color-border);
    font-size: var(--text-sm);
}

.pagination__step {
    font-weight: 600;
    text-decoration: none;
    padding: var(--space-1) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: 999px;
}

.pagination__step:hover {
    background: var(--color-bg);
}

.pagination__step--spent {
    color: var(--color-text-muted);
    border-color: transparent;
}

.pagination__where {
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    min-width: 6rem;
    text-align: center;
}

/* -- the frame a verdict points at (TASK-75) ------------------------------ */

/* Thumbnails wrap rather than scroll: a result has one or two moments,
   occasionally four, and a horizontal scroller for three items hides
   the third behind a gesture. */
.moments {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

.moment__jump {
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas: "still still" "label at";
    gap: var(--space-1) var(--space-2);
    align-items: baseline;
    padding: var(--space-2);
    background: var(--color-surface);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    cursor: pointer;
    font: inherit;
    text-align: left;
    color: inherit;
}

.moment__jump:hover,
.moment__jump:focus-visible {
    border-color: var(--color-text-muted);
    background: var(--color-bg);
}

.moment__still {
    grid-area: still;
    /* The frame decides its own height: these are 9:16 reels and 1:1
       feed posts, and forcing either into the other's box would be
       cropping the evidence. */
    width: 7rem;
    height: auto;
    border-radius: calc(var(--radius) / 2);
    /* A frame that has not arrived yet, or 404s, leaves a box the size
       of the one that would have been there rather than collapsing the
       row it sits in. */
    background: var(--color-bg);
    min-height: 4rem;
}

.moment__label {
    grid-area: label;
    font-weight: 600;
    font-size: var(--text-sm);
}

.moment__at {
    grid-area: at;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}

/* -- the progress bar (TASK-76) ------------------------------------------- */

/* One band per state, the bands summing to the job's posts. Five separate
   tracks answered "how far has fetching got" five times and never
   answered "where are the posts".

   **The colours are a status palette, not a categorical one**, and the
   order they stack in is part of the design. Every band also carries a
   text label in the row beneath, a 2px gap either side, and a name in
   the readout -- colour is never the only channel here.

   **Skipped is orange, not a third grey** (TASK-97). It was #a8a498
   against in-progress's #b9b5a9: ΔE2000 4.8, which is two swatches of
   the same grey. Skipped is not undecided, it is one of the four ways a
   post ends, so it belongs with the outcome colours rather than the
   neutrals -- and it sits between pass and failed in the stack, where
   green | orange | red reads as a progression. Its worst separation
   against any other band, across normal, protanopic and deuteranopic
   vision, goes 4.8 -> 12.6 in light and 8.0 -> 11.8 in dark.

   The dark value is darker than the light one, which inverts the pattern
   the rest of this palette follows. That is deliberate: dark awaiting is
   a pale amber (#e0b155), deuteranopia collapses amber and orange, and
   lightness is the only channel left to separate them by.

   Known and accepted, both pre-dating this: waiting-vs-failed at 9.6
   light / 3.5 dark for protanopia, and pass-vs-failed at 4.4 dark for
   deuteranopia. The waiting band has never rendered on real data.

   **Errors is violet** (TASK-111). The band that was called failed is now
   two: `failed` keeps the red, because a rule failing a post is what the
   red already meant to a reader, and the pipeline breaking gets a colour
   of its own. Violet rather than a sixth warm tone -- red, orange and
   amber are already three, and a fourth is where protanopia and
   deuteranopia stop telling them apart. It sits last in the stack, so it
   never touches orange.

   Measured: the violet's worst separation against any other band is
   ΔE2000 28.9 light and 26.9 dark, across normal, protanopic and
   deuteranopic vision -- comfortably the easiest band in the set to pick
   out, which is right for the one that means something broke. Simulated
   with Vienot's model; the four figures quoted above this paragraph came
   from a different simulation and are not on the same scale.

   The remaining neutrals are deliberately grey: a post that has not been
   decided yet has no verdict colour to wear, and giving it one would say
   the job had got further than it has. */
:root {
    --band-not-started: #e8e6e0;
    --band-waiting: #4a4740;
    --band-in-progress: #b9b5a9;
    --band-awaiting: #9a6a12;
    --band-pass: #1f7a4d;
    --band-skipped: #e8813a;
    --band-failed: #a3312b;
    --band-errors: #6b3f8f;
}

@media (prefers-color-scheme: dark) {
    :root {
        --band-not-started: #2b2b25;
        --band-waiting: #8f8b7e;
        --band-in-progress: #59564a;
        --band-awaiting: #e0b155;
        --band-pass: #4fbf85;
        --band-skipped: #dc6c28;
        --band-failed: #e08079;
        --band-errors: #b98fd6;
    }
}

.progress-figure {
    margin: 0 0 var(--space-4);
}

.progress-bar {
    display: flex;
    /* The 2px surface gap is what separates touching bands -- never a
       border, which would add ink that is not data. */
    gap: 2px;
    height: 1.25rem;
    border-radius: 999px;
    overflow: hidden;
    background: var(--color-bg);
}

.band {
    /* --share is the band's percentage of the job. flex-basis rather than
       width so the gaps come out of the whole rather than out of the
       last band. */
    flex: 0 0 calc(var(--share) * 1%);
    min-width: 2px;
    border: 0;
    padding: 0;
    cursor: default;
    transition: opacity 120ms ease-out;
}

/* Everything else recedes while one band is being read, rather than the
   hovered band growing -- a bar whose parts change size while you point
   at them is a bar you cannot compare.

   Keyed off the card rather than the figure, because half the pointing
   happens in the stat row underneath, which is outside the figure. */
.card:has(.is-lit) .band:not(.is-lit) {
    opacity: 0.3;
}

.card:has(.is-lit) .stat[data-covers]:not(.is-lit) {
    opacity: 0.55;
}

.band.is-lit {
    /* The one place a ring is right: it marks the reading, not the data. */
    box-shadow: inset 0 0 0 2px var(--color-surface);
}

.band--not-started { background: var(--band-not-started); }
.band--waiting { background: var(--band-waiting); }
.band--in_progress { background: var(--band-in-progress); }
.band--awaiting { background: var(--band-awaiting); }
.band--pass { background: var(--band-pass); }
.band--skipped { background: var(--band-skipped); }
.band--failed { background: var(--band-failed); }
.band--errors { background: var(--band-errors); }

.progress-figure__readout {
    margin-top: var(--space-2);
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    /* Fixed height so naming a band never nudges the card, and the stat
       row underneath never moves while being pointed at. */
    min-height: 1.4em;
}

/* The swatch is what makes the stat row a legend: identity arrives as a
   coloured mark beside the words, never as coloured words. */
.stat__swatch {
    display: inline-block;
    width: 0.6rem;
    height: 0.6rem;
    margin-right: 0.4em;
    border-radius: 2px;
    vertical-align: baseline;
}

.stat[data-covers] {
    cursor: default;
    border-radius: var(--radius);
    transition: background 120ms ease-out, opacity 120ms ease-out;
}

.stat.is-lit {
    background: var(--color-bg);
}

/* -- a verdict that came from a model (TASK-83) --------------------------- */

/* Marked, not decorated. This rule has never been scored against
   Samsung's own verdicts, and until TASK-85 runs, a screen that let one
   of its verdicts look like a measured one would be overstating what the
   tool knows. */
.judged {
    margin: 0 0 var(--space-4);
    padding: var(--space-3);
    border: var(--border-width) solid var(--color-review);
    border-radius: var(--radius);
    background: var(--color-bg);
}

.judged__warning {
    margin: 0 0 var(--space-3);
    font-size: var(--text-sm);
    color: var(--color-text);
}

.judged .review-standard {
    margin: 0;
}

/* The transcript can be a paragraph. It is evidence rather than prose to
   read end to end, so it is bounded and scrolls inside itself rather
   than pushing the decision controls down the card. */
.judged__transcript {
    display: block;
    max-height: 7rem;
    overflow-y: auto;
    font-size: var(--text-sm);
}

/* -- the rules reference (TASK-87, TASK-88, TASK-90) ---------------------- */

/* One card per rule. They were rows inside a shared ruleset card,
   divided by a hairline, and a rule whose body is a ten-row matrix ran
   straight into the next rule's title -- at a glance there was no
   telling where one rule ended. A hairline cannot bound a box whose
   height varies by a factor of ten; a border and a gap can.

   The identity column went with it. A 15rem sidebar holding a name and
   a "Change" link cost the matrix a third of its width to save nothing,
   so the body runs the full card and the name sits in a header above
   it. */

.ruleset {
    margin-bottom: var(--space-7);
}

.ruleset__head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}

.ruleset__name {
    margin: 0;
    font-size: var(--text-lg);
}

.ruleset__meta {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--color-text-muted);
}

.ruleset__empty {
    margin: 0;
    padding: var(--space-5);
    border: var(--border-width) dashed var(--color-border);
    border-radius: var(--radius);
    color: var(--color-text-muted);
    font-size: var(--text-sm);
}

.rule {
    background: var(--color-surface);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    margin-bottom: var(--space-4);
    /* The page is reached by fragment -- "Change settings" re-renders it
       with one card open and lands on that card. Without this the card
       butts against the top of the viewport and reads as cut off. */
    scroll-margin-top: var(--space-5);
}

.rule:last-child { margin-bottom: 0; }

/* The card open for editing has to be findable after the jump, so it
   carries the accent the form inside it does. */
.rule--editing {
    border-color: var(--color-accent);
    box-shadow: 0 0 0 1px var(--color-accent);
}

.rule__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    padding: var(--space-4) var(--space-5);
    border-bottom: var(--border-width) solid var(--color-border);
}

.rule__name {
    margin: 0;
    font-size: var(--text-md);
    line-height: 1.2;
}

/* Beside the name it acts on, and saying what it changes. It was a bare
   "Change" under a column of figures, naming neither. */
.rule__edit {
    flex: none;
    font-size: var(--text-sm);
    padding: var(--space-1) var(--space-3);
    border: var(--border-width) solid var(--color-border);
    border-radius: var(--radius);
    text-decoration: none;
}

.rule__edit:hover,
.rule__edit:focus-visible {
    border-color: var(--color-accent);
    text-decoration: underline;
}

.rule__editing {
    flex: none;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-accent);
}

.rule__body { padding: var(--space-5); }

/* The admin's words, set as prose above the settings the page works out
   for itself (TASK-102). Slightly larger than the derived lines below
   it, because it is what a reader came for and those are the detail
   behind it. */
.rule__described {
    font-size: var(--text-sm);
    margin-bottom: var(--space-4);
    max-width: 44rem;
}

.rule__described p {
    margin: 0 0 var(--space-2);
}

.rule__described ul {
    margin: 0 0 var(--space-2);
    padding-left: 1.1rem;
}

.rule__described li {
    margin-bottom: var(--space-1);
}

.rule__described > :last-child {
    margin-bottom: 0;
}

.rule__lines {
    margin: 0;
    padding-left: 1.1rem;
    font-size: var(--text-sm);
}

.rule__lines li { margin-bottom: var(--space-1); }
.rule__lines li:last-child { margin-bottom: 0; }

.rule__more {
    margin-top: var(--space-2);
    font-size: var(--text-sm);
}

.rule__more:first-child { margin-top: 0; }

.rule__more summary {
    cursor: pointer;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
}

.rule__more ul {
    margin: var(--space-2) 0 0;
    padding-left: 1.1rem;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
}

/* The matrix is ten rows of three short values: dense, tabular, and
   numerically aligned so the shapes column reads as a column. It had
   0.2rem of vertical air and no left padding, which ran the columns
   together -- banded rows and real padding on both axes instead. */
.matrix__wrap { overflow-x: auto; }

.matrix {
    border-collapse: collapse;
    font-size: var(--text-sm);
    width: 100%;
    max-width: 40rem;
}

.matrix th,
.matrix td {
    text-align: left;
    padding: var(--space-2) var(--space-5);
}

.matrix th {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    font-weight: 600;
    border-bottom: 2px solid var(--color-border);
}

/* Banding rather than a rule per row: ten hairlines in a box that is
   itself bounded by a hairline is a lot of lines for three columns. */
.matrix tbody tr:nth-child(even) { background: var(--color-bg); }

.matrix td:last-child {
    font-variant-numeric: tabular-nums;
}

/* -- changing a rule ------------------------------------------------------ */

.rule-form {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: var(--border-width) solid var(--color-border);
}

.rule-form:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

.rule-field { margin-bottom: var(--space-4); }

.rule-field label {
    display: block;
    font-weight: 600;
    font-size: var(--text-sm);
    margin-bottom: var(--space-1);
}

.rule-field input[type="text"],
.rule-field input[type="number"],
.rule-field textarea {
    width: 100%;
    max-width: 34rem;
    font-family: inherit;
    font-size: var(--text-sm);
}

.rule-field textarea {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.rule-field__help {
    margin: var(--space-1) 0 0;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    max-width: 34rem;
}

.rule-field--wrong label { color: var(--color-fail); }

.rule-field--wrong ul.errorlist {
    margin: var(--space-1) 0 0;
    padding-left: 1.1rem;
    color: var(--color-fail);
    font-size: var(--text-sm);
}

.rule-form__actions {
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

/* Narrow screens: the card keeps its padding but loses the horizontal
   generosity, and the matrix scrolls inside its own wrapper rather than
   pushing the page sideways. */
@media (max-width: 46rem) {
    .rule__head,
    .rule__body { padding: var(--space-4); }

    .matrix th,
    .matrix td { padding: var(--space-2) var(--space-4); }
}

/* The prompt a judged rule sends (TASK-116). Pre, because it is text with
   its own line breaks and paragraph structure that the model receives
   verbatim -- reflowing it would show something other than what is sent.
   Wrapped rather than scrolled sideways: it is prose, not code. */
.rule__prompt-body {
    margin: var(--space-2) 0 0;
    padding: var(--space-3);
    background: var(--color-surface-sunken, rgba(127, 127, 127, 0.08));
    border-radius: var(--radius-sm, 4px);
    font-family: inherit;
    font-size: var(--text-sm);
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* A form field switched off, not removed (TASK-134: the New Job
   spreadsheet upload during the beta). Needed because `.field input`
   above sets its own background and colour, which overrides the
   browser's own disabled styling -- without this the field looks
   perfectly usable and only refuses when clicked. */
.field input:disabled,
.field input.field__input--off {
    background: var(--color-surface-sunken, rgba(127, 127, 127, 0.08));
    color: var(--color-text-muted);
    cursor: not-allowed;
}
