/* Fieldwork: one stylesheet, no framework, no build step. */

/* ── Fonts ────────────────────────────────────────────────────────────────── */
/* Stored in the app rather than fetched from Google, so a page never waits on
   a third party and the content security policy stays 'self'. Both are the
   variable cuts from Fontsource, latin and latin-ext (Polish and Czech names
   are common on the tenant), under the SIL Open Font License beside them in
   public/fonts. Instrument Sans carries everything a person reads, titles and
   big figures included: a serif for those was tried in the mockups and
   rejected. JetBrains Mono is for ids, dates and provenance, never prose. */
@font-face { font-family: "Instrument Sans"; font-style: normal; font-weight: 400 700; font-display: swap; src: url(/fonts/instrument-sans-latin-wght-normal.woff2) format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Instrument Sans"; font-style: normal; font-weight: 400 700; font-display: swap; src: url(/fonts/instrument-sans-latin-ext-wght-normal.woff2) format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Instrument Sans"; font-style: italic; font-weight: 400 700; font-display: swap; src: url(/fonts/instrument-sans-latin-wght-italic.woff2) format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Instrument Sans"; font-style: italic; font-weight: 400 700; font-display: swap; src: url(/fonts/instrument-sans-latin-ext-wght-italic.woff2) format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url(/fonts/jetbrains-mono-latin-wght-normal.woff2) format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url(/fonts/jetbrains-mono-latin-ext-wght-normal.woff2) format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }

/* ── The palette ──────────────────────────────────────────────────────────── */
/* Light, calm and exact (the redesign of 2026-09-22, mockups on the design
   canvas linked from .claude/rules/frontend.md): a light grey page, white
   panels, dark navy text, and the Proteus dashboard's navy bar across the
   top, which is the piece a colleague recognises. Colour is kept for two
   jobs, what needs a person (the accent) and how things stand (the three
   states); type and space do the rest. There is one theme and no switch.
   color-scheme makes the widgets the browser paints itself (scrollbars,
   checkboxes, the autofill wash) follow the page rather than the OS. */
:root {
    color-scheme: light;

    --bg: #f3f5f8;
    --surface: #ffffff;
    /* Selected, pressed, the segmented control's chosen half. */
    --raised: #e8eef6;
    /* Hairlines are the only borders; controls take the stronger edge, so a
       button reads as something to press and a panel does not. */
    --border: #e3e8ef;
    --border-strong: #d3dae4;
    --text: #0f1b2d;
    /* Labels and quotes, a step under the text; muted is dates, ids, hints. */
    --text-2: #4a5b72;
    --muted: #637289;
    /* A tag's quiet fill: a source named by its icon rather than a colour. */
    --chip: #eef2f7;
    --chip-fg: #1f2e44;
    /* The block a pair of photos sits on (the Photos tab): the chip's fill,
       named for the job so restyling a tag cannot repaint it. */
    --photo-pair: var(--chip);

    /* One accent: what needs a person, the main button, a link. */
    --accent: #2f6feb;
    --accent-hover: #1f55c4;
    --accent-tint: color-mix(in srgb, var(--accent) 12%, transparent);
    --accent-wash: color-mix(in srgb, var(--accent) 4%, transparent);

    /* The three states come in two strengths. The plain token is dark enough
       to read as small text on white, which is how most of them are used; the
       -mark one is the brighter colour for a dot, a bar or a fill, where
       there are no letters to read. */
    --ok: #16794a;
    --ok-mark: #1f9d57;
    --warn: #8a5a00;
    --warn-mark: #d08b0b;
    --error: #b42f28;
    --error-mark: #d6453d;

    /* The main button is the accent, one per view at most. Named for the job
       rather than reusing --accent, so a change to link colour cannot silently
       repaint every button. */
    --btn-primary-bg: var(--accent);
    --btn-primary-fg: #ffffff;
    --btn-primary-hover: var(--accent-hover);

    /* Review, design D. The navy square that marks "replaces Fieldwork's own
       value", the same navy the topbar wears, but named for the job so a
       change to the bar's colour cannot silently repaint this mark. The three
       tints are a field row's state under All fields: needs a person, decided
       (approved by a person or auto-accepted; one tint for both, since the
       stripe reads by state and the row's own text says which), and decided
       off (rejected or reported). Mixed from the tokens that already mean
       those things, so no new literal is needed for them. */
    --fw-mark: #06213b;
    /* The tree drawn down a group's fields on Review: a shade between the
       hairline and the strong border, as the boards draw it. */
    --tree-line: #b3bdcb;
    --fw-mark-fg: #ffffff;
    --tint-need: color-mix(in srgb, var(--accent) 7%, transparent);
    --tint-need-line: color-mix(in srgb, var(--accent) 22%, transparent);
    --tint-ok: color-mix(in srgb, var(--ok-mark) 7%, transparent);
    --tint-ok-line: color-mix(in srgb, var(--ok-mark) 22%, transparent);
    --tint-off: #f5f7fa;
    --tint-off-line: var(--border-strong);

    /* The bar keeps its own tokens: nothing outside it may use them, and
       nothing inside it may use the page's, which are cut for white. */
    --topbar: #06213b;
    --topbar-line: #123457;
    --topbar-dim: #93a7bf;
    --topbar-soft: #c7d3e3;
    --topbar-fg: #ffffff;
    --topbar-hover: rgba(255, 255, 255, .09);
    --topbar-mark: #8db4ff;
    --topbar-arrow: #3e5a7e;
    --topbar-phase: #edb65a;
    /* The badge on Review: the accent's blue, named for the bar because only
       the bar's tokens may be used inside it, and meaning what the accent
       means everywhere else, that a person is needed. */
    --topbar-badge: #2f6feb;

    /* The test copy's band across the top of every page (TEST_MODE in
       config.js), and its note on the sign-in page. A yellow nothing else on
       the site uses, so a screen of the copy is never read as the live site. */
    --test-band: #f7cf4f;
    --test-band-fg: #2b2100;
    --test-band-line: #d9a90f;

    /* A panel sits a hair off the page; a popover floats well clear of it. */
    --shadow-card: 0 1px 2px rgba(15, 27, 45, .05);
    --shadow: rgba(15, 27, 45, .16);

    --font: "Instrument Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    /* 14 for a panel, 8 for a control, round for a pill. */
    --radius: 14px;
    --radius-sm: 8px;
}

* { box-sizing: border-box; }

/* The UA stylesheet hides [hidden] with display:none, which any author rule
   setting display beats. .button sets display:inline-block, so a button the
   markup or a script hid stayed on screen. Global because the trap is: every
   component that sets display is one hidden attribute away from the same bug. */
[hidden] { display: none !important; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 14px/1.5 var(--font);
    -webkit-font-smoothing: antialiased;
}

body.centered {
    display: flex;
    min-height: 100vh;
    align-items: center;
    justify-content: center;
    padding: 24px;
}

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Topbar ───────────────────────────────────────────────────────────────── */
/* Navy, like the Proteus dashboard's. Two ways in (Overview, Records) as
   plain words, then the four stages boxed as one group, numbered and joined
   by chevrons, because their order is the point; the admin plumbing sits
   apart on the right, then who is signed in. */
.topbar {
    display: flex;
    align-items: center;
    gap: 22px;
    padding: 0 28px;
    height: 56px;
    background: var(--topbar);
    border-bottom: 1px solid var(--topbar-line);
    color: var(--topbar-fg);
}
.topbar .brand { display: flex; align-items: center; gap: 10px; flex: none; color: var(--topbar-fg); text-decoration: none; }
.topbar .brand-mark { flex: none; color: var(--topbar-mark); }
.topbar .brand-name { font-size: 18px; font-weight: 700; letter-spacing: -.01em; }
/* The pilot's phase, beside the brand on every page. Amber because it is a
   standing caution rather than an error: nothing here has reached Vincere. */
.topbar .phase {
    flex: none; margin-left: -10px;
    padding: 3px 9px; border-radius: 999px;
    background: color-mix(in srgb, var(--topbar-phase) 12%, transparent); color: var(--topbar-phase);
    font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
/* A way in is a plain word, and only the pipeline is boxed. Every link wore
   a padded pill of its own until 2026-09-23, which gave the bar nine boxes
   and made it the heaviest thing on the screen; Kevin's reading against the
   mockup was that he liked the bar there better, and this is the difference.
   The words also sit a step brighter than they did, at --topbar-soft: they
   are the way around the app, not a caption. */
.topbar nav { display: flex; align-items: center; gap: 22px; flex: 1; font-size: 14px; }
.topbar nav a {
    display: flex; align-items: center; gap: 7px;
    color: var(--topbar-soft); font-weight: 500; text-decoration: none; white-space: nowrap;
    transition: color .1s, background-color .1s;
}
.topbar nav a:hover { color: var(--topbar-fg); }
.topbar nav a.active { color: var(--topbar-fg); font-weight: 600; }
/* The box is the machine: its outline is the bar's own divider, quieter than
   the words inside it. */
.topbar nav .pipeline {
    display: flex; align-items: center; gap: 4px;
    padding: 4px 6px;
    border: 1px solid var(--topbar-line); border-radius: 10px;
}
/* Inside the box a stage is a pill again, because one of the four is always
   the page being read and the box is what says these four are one thing. */
.topbar nav .pipeline a { padding: 4px 10px; border-radius: 7px; color: var(--topbar-dim); font-weight: 500; }
.topbar nav .pipeline a:hover { color: var(--topbar-fg); background: var(--topbar-hover); }
.topbar nav .pipeline a.active { color: var(--topbar-fg); background: var(--topbar-hover); font-weight: 600; }
.topbar nav .step { font-family: var(--mono); font-size: 11px; opacity: .7; }
/* How many wait on Review, as a pill after the word: the one figure the bar
   carries, because the queue is what the bar leads to. */
.topbar nav .nav-badge {
    flex: none; margin-left: 1px; padding: 1px 8px; border-radius: 999px;
    background: var(--topbar-badge); color: var(--topbar-fg);
    font-size: 12px; font-weight: 600; line-height: 1.45; font-variant-numeric: tabular-nums;
}
/* A connector, not a link. Named nav-arrow rather than flow: the flow strips
   further down own that word. */
.topbar nav .nav-arrow { flex: none; color: var(--topbar-arrow); }
.topbar nav.admin { flex: none; gap: 22px; font-size: 14px; }
.topbar .topbar-rule { flex: none; width: 1px; height: 24px; background: var(--topbar-line); }
/* A batch runs on the server rather than in the page that started it, so the
   bar is where "still going" belongs: it is the one thing on screen wherever a
   person walks to while they wait. The dot pulses because a line of text that
   has stopped changing looks exactly like a page that has stopped asking.
   Green and still once the batch has landed. */
.topbar .run-live {
    display: flex; align-items: center; gap: 8px;
    padding: 3px 11px;
    border: 1px solid var(--topbar-line); border-radius: 999px;
    color: var(--topbar-soft);
    font-size: 12px; text-decoration: none; white-space: nowrap;
    /* A candidate with a long name must not push Sign out off the bar, so the
       chip gives up its own text before it takes anyone else's room, and the
       name is the half that gives: how far through and how long it has been
       are the reason anybody looks. */
    max-width: 340px; min-width: 0;
}
.topbar .run-live .run-live-what { overflow: hidden; text-overflow: ellipsis; }
.topbar .run-live .run-live-count { flex: none; color: var(--topbar-dim); }
.topbar .run-live .run-live-count::before { content: "· "; }
.topbar .run-live:hover { color: var(--topbar-fg); }
.topbar .run-live .dot { width: 7px; height: 7px; margin: 0; border-radius: 50%; background: var(--topbar-mark); animation: run-pulse 1.4s ease-in-out infinite; }
.topbar .run-live.done .dot { background: var(--ok-mark); animation: none; }
@keyframes run-pulse { 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .topbar .run-live .dot { animation: none; } }
/* The name and the way out, and no initial in a circle: there is one person
   on this tool and a letter in a disc told nobody anything (Kevin,
   2026-09-23). */
.topbar .user { display: flex; align-items: center; gap: 14px; flex: none; color: var(--topbar-fg); font-size: 14px; font-weight: 500; }
.topbar .user form { margin: 0; }
.topbar .user button.link { color: var(--topbar-dim); text-decoration: none; font-size: 13px; }
.topbar .user button.link:hover { color: var(--topbar-fg); }
.topbar .user .user-switch { color: var(--topbar-dim); text-decoration: none; font-size: 13px; }
.topbar .user .user-switch:hover { color: var(--topbar-fg); }
/* ── Waiting on the server ───────────────────────────────────────────────── */
/* Drawn by apiFetch in common.js on every page, whenever a read has taken
   longer than a moment or the server has stopped answering. It floats over the
   page rather than sitting in it, because the page's own "Loading…" lines are
   muted text that reads the same whether the server is working or has gone.
   Accent while loading; amber while the server is away, which is a caution
   the person may have to act on if it lasts. The line along the top edge and
   the dot both move, so a wait that is still going never looks finished. */
.wait-line { position: fixed; z-index: 55; top: 0; left: 0; right: 0; height: 3px; overflow: hidden; pointer-events: none; }
.wait-line::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: -30%; width: 30%;
    background: var(--accent); animation: wait-slide 1.3s ease-in-out infinite;
}
.wait-card {
    position: fixed; z-index: 55; top: 70px; left: 50%; transform: translateX(-50%);
    display: flex; align-items: baseline; gap: 10px;
    width: max-content; max-width: min(620px, calc(100vw - 24px));
    padding: 10px 14px;
    background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
    box-shadow: 0 8px 28px var(--shadow);
    font-size: 13px; line-height: 1.5; color: var(--text);
}
.wait-dot { flex: none; align-self: center; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: run-pulse 1.4s ease-in-out infinite; }
.wait-secs { flex: none; color: var(--muted); font-variant-numeric: tabular-nums; }
.wait.restarting .wait-card { border-color: var(--warn-mark); }
.wait.restarting .wait-dot,
.wait.restarting .wait-line::before { background: var(--warn-mark); }
@keyframes wait-slide { to { left: 100%; } }
@media (prefers-reduced-motion: reduce) {
    .wait-dot { animation: none; }
    .wait-line::before { animation: none; left: 0; width: 100%; }
}
/* ── Layout ───────────────────────────────────────────────────────────────── */
.page { max-width: 1000px; margin: 0 auto; padding: 40px 28px 72px; }
/* Three widths, and each one has to be earned: 1000 for a page that is mostly
   words (Overview, Connection), 1500 for everything with a table in it, which
   is every other page, and 1850 for a table with more columns than 1500 can
   show without a cell being cut: the file archive's nine, and the email
   archive's eight, two of which are a subject and an address.
   1500 was 1200 until 2026-09-05, when Kevin measured 1152 of content on a
   1536-wide laptop and asked for the rest: this is a business tool that
   nobody opens on a phone, and the file archive's record column was clipping
   names for want of 300px that were sitting empty. 1850 followed on
   2026-09-08 for the same reason on a wider screen: on a 1536 laptop it
   changes nothing, the viewport being the smaller cap. The narrow one was
   900 until the redesign of 2026-09-22 widened it for the four stage tiles.
   Widening the page is only half of it, though. A line of prose 1,452px
   long cannot be read, so the paragraphs, and only the paragraphs, keep a
   measure of about 100 characters; the tables take the whole width. */
.page.wide { max-width: 1500px; }
.page.widest { max-width: 1850px; }
.page.wide > p, .page.widest > p { max-width: 100ch; }
/* Titles and big figures share one voice: the sans at 600, drawn in a
   little. The page title leads, and the sentence under it is 16 in the
   secondary colour, one sentence where it can be; the long explanation
   belongs behind the (i) beside the title. */
h1 { margin: 0 0 8px; font-size: 36px; line-height: 1.1; font-weight: 600; letter-spacing: -.025em; }
h1 + p, .crumbs + h1 + p { margin: 6px 0 0; font-size: 16px; color: var(--text-2); }
h1 + p.muted { color: var(--text-2); }
h2 { margin: 0 0 10px; font-size: 15px; font-weight: 600; }
.section-h2 { margin: 36px 0 4px; font-size: 22px; line-height: 1.2; letter-spacing: -.02em; }
h1 button.help, .section-h2 button.help { vertical-align: 6px; }

/* Sub-pages of a stage (an archive browser under Sources) say where they sit. */
.crumbs { margin: 0 0 10px; font-size: 13px; color: var(--muted); }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--text); text-decoration: underline; }
.crumbs span { margin: 0 6px; }
.muted { color: var(--muted); }
.small { font-size: 13px; }

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card);
    padding: 22px 24px;
    margin: 20px 0;
}
/* A paragraph at either edge of a card keeps to the card's own padding:
   a one-line empty state ("Nothing is running.") was a tall box otherwise. */
.card > p:first-child { margin-top: 0; }
.card > p:last-child { margin-bottom: 0; }
/* A panel whose own blocks carry the padding, because they divide it: a run
   line over a fold over a table over a foot, each with a hairline between and
   the foot filling to the bottom corners. The card's 22px would sit outside
   every one of those rules and leave the foot floating inside a white margin. */
.card.flush { padding: 0; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 20px 0; }
.cards .card { margin: 0; }

/* ── The page frame ───────────────────────────────────────────────────────── */
/* Every page opens the same way and is built of the same three pieces (the
   second pass of the redesign, 2026-09-22). Kevin's reading of the first
   pass was that the components had been redrawn but the pages themselves
   were still messy, and they were: eight pages opened eight different ways,
   three of them with no sentence at all and two with a paragraph of three
   lines, a status pill sat loose under the prose on one, and inside them
   every block carried the same weight, so nothing said what a page was for.

   The pieces are: a `.page-head` (the title with its (i), one sentence, and
   at the far end the one state or action the page is about, over a
   hairline); a `.sec` per section (a 22px heading outside the panel, so the
   page has a rhythm a card title in 15px bold could never give it); and a
   `.toolbar` for the row of filters a list is read through. What the
   sections hold is unchanged: the volume of text on these pages is the
   point of the tool, so prose moves behind the (i) marks and into folds
   rather than being cut. */
/* The title line alone: no sentence and no rule under it since the second
   look of 2026-09-22 (Kevin: "is that line really necessary? I would like
   the title and then just the tabs directly underneath it"). What a page is
   for is behind its (i); the tabs, the first figures or the first section
   heading say the rest. Overview alone keeps a sentence, being the one page
   that describes the whole tool. */
.page-head {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 24px;
    margin: 0 0 22px;
}
.page-head + .tabs, .page-head + .sec { margin-top: 0; }
/* A title with nothing under it gives up its own bottom margin, so the gap
   to the tabs is the head's 22px rather than 22 plus the heading's 8. */
.page-head-main > h1:last-child { margin-bottom: 0; }
.page-head-main { flex: 1 1 420px; min-width: 0; }
/* The measure the wide pages give their paragraphs, applied here because the
   sentence is no longer a direct child of .page. */
.page-head p { max-width: 95ch; }
/* The state or the main action, at the far end of the title's own line:
   a page says how it stands where its name is, not in a pill under the
   prose. */
.page-head-side { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-left: auto; padding-bottom: 4px; }
.page-head-side .status { margin: 0; }

/* A section: the heading stands on the page and the panel under it holds
   only content, which is what gives a long page its rhythm. A card that is
   a tile in a `.cards` grid keeps its own 15px title; a card that is a
   section's body is named by the heading above it and carries none. */
.sec { margin: 34px 0 0; }
.sec-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin: 0 0 12px; }
.sec-head h2, .section-h2 { margin: 0; font-size: 22px; line-height: 1.2; font-weight: 600; letter-spacing: -.02em; }
.section-h2 { margin: 34px 0 12px; }
.sec-head .help, .sec-head .status { align-self: center; }
.sec-head .status { margin: 0; }
/* Whatever the heading introduces sits straight under it; anything after
   that keeps its own spacing, so two folded cards in one section still read
   as two. */
.sec > .sec-head + * { margin-top: 0; }

/* A paragraph inside a tab panel is prose like any other and keeps the same
   measure the page's own paragraphs do: a wide page is wide for its tables. */
[role="tabpanel"] > p { max-width: 100ch; }

/* A section that belongs to the page rather than to the tab above it, at the
   foot: a rule across the page says the tab's panel has ended. */
.sec-statistics { margin-top: 56px; padding-top: 28px; border-top: 1px solid var(--border); }

/* The row of controls a list is read through: filters left, the count of
   what they left at the far end. */
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin: 0 0 14px; }
.toolbar .end { margin-left: auto; }
.toolbar .count { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; }

/* A strip of figures a page or a section leads with: the figure in the
   title's voice, the thing it counts above it in small capitals. One panel
   with rules between the cells, so four facts read as one statement rather
   than as four boxes of equal weight. */
.stats {
    display: flex; flex-wrap: wrap;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-card);
}
/* A strip that is a fold's whole body already has the card's surface under
   it, so it drops its own panel: a box inside a box is what the redesign
   took out of these pages. */
details.fold .stats { background: none; border: 0; box-shadow: none; }
details.fold .stats .stat:first-child { padding-left: 0; }
.stat { flex: 1 1 0; min-width: 160px; padding: 15px 20px 17px; border-left: 1px solid var(--border); }
.stat:first-child { border-left: 0; }
.stat-figure { margin: 5px 0 0; font-size: 26px; line-height: 1.1; font-weight: 600; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }
.stat-note { margin: 5px 0 0; font-size: 13px; color: var(--muted); }
.stat .status { margin: 6px 0 0; }
/* The one cell of a strip that carries the accent: the figure a person is
   meant to act on, which on the extraction queue is how many records are
   due. Accent means "needs a person" everywhere on the app, so no more than
   one cell of a strip may take it. */
.stat.lead .stat-figure, .stat.lead .stat-figure a { color: var(--accent); }
/* A figure that is also a link keeps the figure's voice; the underline on
   hover is what says it leads somewhere. */
.stat-figure a { color: inherit; text-decoration: none; }
.stat-figure a:hover { text-decoration: underline; }
/* The voice every label, column head and eyebrow on the app speaks in. */
.cap { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.cap button.help { vertical-align: 1px; }

/* ── Sources: systems as tabs, and the sources inside them ────────────────── */
/* Two levels: a system is where a fetcher connects (Vincere, Granola), a source
   is one kind of text it brings back. Systems were collapsed blocks until
   2026-08-28, so the page opened on nothing and Vincere, where every source
   built so far lives, cost a click on every visit. They are tabs instead, one
   system selected at all times, laying its sources out side by side with each
   fetcher's activity folded in at the bottom. A source that is not built yet
   keeps the same column, drawn muted, so the page shows where the next fetcher
   lands before it exists. */
/* Tabs are a tray: a sunken strip the shade of a raised surface, and the
   chosen tab a white pill standing off it with a hairline and the card's
   own shadow, so it reads as the thing on top. They were words on a
   hairline with an accent underline for the first half of 2026-09-22, and
   Kevin's reading was that they did not stand out as tabs; before that they
   were sheets opening into their panel, which on a light page read as a
   folder tab. The tray is the shape a segmented control has everywhere
   else, so the eye knows it before reading a word, and it sits directly
   under the title with no sentence between. The Review filter under it is
   a joined button group rather than a second tray, so the two levels of
   choice never look like the same control twice. */
.tabs {
    display: inline-flex; flex-wrap: wrap; gap: 2px;
    margin: 22px 0 0; padding: 3px;
    background: var(--raised); border: 1px solid var(--border); border-radius: 11px;
}
.tab {
    display: inline-flex; align-items: baseline; gap: 8px;
    padding: 7px 15px;
    border: 1px solid transparent; border-radius: 8px;
    background: none; color: var(--text-2);
    font: inherit; font-size: 14px; font-weight: 500; line-height: 1.3; cursor: pointer;
}
.tab:hover { color: var(--text); }
.tab[aria-selected="true"] {
    color: var(--text); font-weight: 600;
    background: var(--surface); border-color: var(--border); box-shadow: var(--shadow-card);
}
.tab-count { color: var(--muted); font-size: 12.5px; font-weight: 400; font-variant-numeric: tabular-nums; }
.tab[aria-selected="true"] .tab-count { color: var(--text-2); }

.system {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-card);
    margin: 20px 0 14px;
}

/* A system with one source that is not built yet (Granola) is still a block of
   prose in a panel, so it keeps the simple layout the three Vincere sources
   had until 2026-09-09. */
.system-body { display: grid; }
.system-body.cols-1 { grid-template-columns: minmax(0, 1fr); }
.source-col { padding: 16px 18px 18px; min-width: 0; }
.source-col h2 { margin: 0 0 2px; font-size: 15px; font-weight: 650; }
.source-col.planned h2 { color: var(--muted); }
.source-count { color: var(--muted); font-size: 13px; font-weight: 400; font-variant-numeric: tabular-nums; }

/* ── The sources table ────────────────────────────────────────────────────── */
/* The three Vincere sources answer the same questions, so the panel is one
   table: a row is a question asked once, and the three sources are the columns
   that answer it. They were three independent `.kv` lists side by side until
   2026-09-09, each sized to its own longest label, and Kevin's reading was that
   nothing lined up. It did not: the columns held 7, 11 and 8 rows, "Range" was
   the second row of one and the seventh of another, and since the columns ran
   to different lengths the three Fetch buttons sat 263px apart with a rule
   down the middle of the gap.

   Built on table.calls' conventions rather than table.browse's: an archive
   browser is one item a row, and this is the transpose of that, one fact a row
   and one source a column. What it borrows is the muted label column, the
   group heading that is a label and air rather than a filled band, and the
   rule down the left of each column group. */
table.sources { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
table.sources th, table.sources td { text-align: left; padding: 6px 14px; border-bottom: 1px solid var(--border); vertical-align: baseline; }
table.sources tbody th { color: var(--muted); font-weight: 500; }
table.sources td { font-variant-numeric: tabular-nums; }
/* The divider the three columns had before, now a rule per cell, since a
   collapsed border cannot be drawn down a grid that no longer exists. */
table.sources th.src, table.sources td.src { border-left: 1px solid var(--border); }
/* The empty corner above the label column carries no rule and no floor: it is
   the one cell of the table that is not about anything. */
table.sources thead td:first-child { border: 0; }

/* The heading is the source: its name, how it is doing, and the controls that
   act on it, each on its own line so the three heads are one shape and the
   state word sits at the same height across all three. The count that used to
   sit here went on 2026-09-09: "Items" is the first row of the table below,
   and the same number twice, six lines apart, is not a heading. */
table.sources thead th.src { padding: 14px 14px 14px; }
/* The pill at its ordinary size: at 15px, matched to the name beside it, it
   outweighed the name and every other pill in the app (Kevin, 2026-09-22). */
table.sources thead .status { margin: 6px 0 0; }
/* The buttons sit under the state they act on rather than at the foot of the
   column, where fourteen rows separated them from the reason for pressing
   them. */
table.sources thead .actions { margin-top: 12px; }

/* A group heading: a label and the air above it, never a filled band, which
   would compete with the figures the grouping exists to organise. */
/* Small capitals, the voice every label and column head in the app speaks in,
   not the dark, life-sized words this read as until Kevin asked for the group
   names to sit as quietly as the rows they head (2026-09-22). */
table.sources tbody.t-group tr.g-head th { padding-top: 18px; font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
table.sources tbody.t-group tr.g-head th, table.sources tbody.t-group tr.g-head td { border-bottom: 0; padding-bottom: 2px; }
/* Fourteen round marks down one column is speckle, and it is what the label
   column had become: the eye reads the marks before the labels. A row's own
   mark fades in when the pointer is on the row, and the group heads keep
   theirs, so the page is quiet at rest and nothing is out of reach. The
   keyboard is not asked to hover: a focused mark shows itself. */
table.sources tbody th button.help { opacity: 0; transition: opacity .1s; }
table.sources tbody tr:hover th button.help,
table.sources tbody th button.help:focus-visible { opacity: 1; }
/* A fact that belongs to every source at once (the daily API quota) is written
   in the shared label column rather than three times over. */
table.sources .row-note { display: block; color: var(--muted); font-weight: 400; font-size: 12px; font-variant-numeric: tabular-nums; }

/* The one cell that carries colour: a fetcher's last error. `.warn` on its own
   is not a colour anywhere in this stylesheet (it qualifies .status, .tag and
   a table.calls cell), so the table says what amber means inside it. */
table.sources .warn { color: var(--warn); }

/* The one row of the table that is a headline rather than a fact: how much
   each source holds is what the page is opened to see, so it takes the
   figure voice every other headline number on the app has since 2026-09-22.
   The heading above it still carries no count, for the reason it lost one
   on 2026-09-09: the same number twice, six lines apart, is not a heading. */
table.sources td.figure { font-size: 22px; font-weight: 600; letter-spacing: -.025em; line-height: 1.2; }
table.sources .tally { font-size: 13px; }
/* A breakdown's count sits just off its name rather than at the far edge of a
   340px cell, where the eye loses the line between the two. */
table.sources .tally { max-width: 22em; }

details.activity { margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--border); }
details.activity > summary { margin-top: 0; color: var(--text); font-size: 13px; font-weight: 600; }
details.activity > summary:hover { color: var(--accent); }

.login { width: min(420px, 100%); padding: 32px 32px 30px; }
.login h1 { margin-bottom: 4px; font-size: 32px; }
.login .brand-mark { display: block; margin: 0 0 18px; color: var(--accent); }
/* "Who are you?" after signing in with a shared account: one button per
   name, two to a row, the name already picked in the accent. */
.who-people { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0 16px; }
.who-people .button { padding: 10px 14px; font-size: 14px; }
.who-note { margin: 0 0 12px; }

/* ── Bits ─────────────────────────────────────────────────────────────────── */
/* How something stands: always a dot and a word, never colour alone, in a
   tinted pill the colour of the state (the redesign of 2026-09-22; it was a
   17px coloured word until then). The dot is the bright -mark colour, the
   word the darker one that reads as text on white. */
.status {
    display: inline-flex; align-items: center; gap: 7px;
    margin: 4px 0 10px; padding: 3px 11px 3px 9px; border-radius: 999px;
    background: var(--chip); color: var(--text-2);
    font-size: 13px; font-weight: 600; line-height: 1.5;
}
.status::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
.status.ok { background: color-mix(in srgb, var(--ok-mark) 11%, transparent); color: var(--ok); }
.status.ok::before { background: var(--ok-mark); }
.status.warn { background: color-mix(in srgb, var(--warn-mark) 13%, transparent); color: var(--warn); }
.status.warn::before { background: var(--warn-mark); }
.status.error { background: color-mix(in srgb, var(--error-mark) 11%, transparent); color: var(--error); }
.status.error::before { background: var(--error-mark); }
/* A state in a cell of a list drops the pill and keeps the dot: a column of
   filled pills is colour down the whole column, which is what the redesign
   took off the Records directory for the same reason. The word stays in the
   page's own ink, the dot carrying the colour. */
.status.plain { margin: 0; padding: 0; background: none; color: var(--text); }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: 8px 22px; margin: 12px 0 0; font-size: 13px; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; font-variant-numeric: tabular-nums; }
.kv .status { margin: 0; }

/* A breakdown with too many buckets to read as a sentence: name left, count
   right, one per line. display: contents on the li puts both spans straight
   into the grid, which is what keeps the counts in a column. */
.tally { display: grid; grid-template-columns: 1fr max-content; gap: 1px 12px; margin: 0; padding: 0; list-style: none; }
.tally li { display: contents; }
.tally .n { text-align: right; font-variant-numeric: tabular-nums; }

.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 14px; }

/* Buttons: an outline for everything, the accent for the one main action a
   view has, red outline for the destructive one, and a plain accent word for
   "show me more". */
.button {
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    line-height: 1.4;
    padding: 7px 14px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--chip-fg);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color .1s, border-color .1s;
}
.button:hover { background: var(--bg); border-color: var(--muted); color: var(--text); }
.button:disabled { opacity: .5; cursor: default; }
.button:disabled:hover { background: var(--surface); border-color: var(--border-strong); }
.button.primary { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); color: var(--btn-primary-fg); font-weight: 600; }
.button.primary:hover { background: var(--btn-primary-hover); border-color: var(--btn-primary-hover); color: var(--btn-primary-fg); }
.button.primary:disabled:hover { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); }
/* Red at rest, not only on hover: rejecting is the one destructive thing on the
   review page. Mixed from --error rather than named, so the wash and the edge
   follow the one red without a second pair of literals to keep in step. */
.button.danger { color: var(--error); border-color: color-mix(in srgb, var(--error-mark) 45%, transparent); }
.button.danger:hover { background: color-mix(in srgb, var(--error-mark) 8%, transparent); border-color: var(--error-mark); color: var(--error); }
.button.block { display: flex; width: 100%; text-align: center; }

button.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; font-size: 13px; font-weight: 500; padding: 0; text-decoration: none; }
button.link:hover { color: var(--accent-hover); text-decoration: underline; }

/* Inputs: the page's grey inside a white panel, a hairline edge, the label
   above. */
input[type="text"], input[type="password"], input[type="search"], input[type="number"], input[type="date"], input[type="email"], select, textarea {
    font: inherit;
    font-size: 14px;
    padding: 7px 11px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    width: 100%;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); background: var(--surface); }
select, input[type="search"] { width: auto; min-width: 220px; }
select {
    appearance: none; padding-right: 32px; cursor: pointer;
    background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(-45deg, transparent 50%, var(--muted) 50%);
    background-size: 5px 5px, 5px 5px;
    background-position: calc(100% - 16px) 55%, calc(100% - 11px) 55%;
    background-repeat: no-repeat;
}
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
label { display: block; font-size: 13px; color: var(--text-2); margin: 12px 0 5px; }
.check { display: flex; align-items: center; gap: 6px; margin: 0; }
.check input { width: auto; }
.stack { display: block; }
/* The submit button sits directly under the last input, which reads as one
   glued control without a gap. */
.stack .button { margin-top: 16px; }

.alert { padding: 10px 14px; border-radius: 10px; font-size: 14px; border: 1px solid transparent; }
.alert.error { background: color-mix(in srgb, var(--error-mark) 8%, transparent); border-color: color-mix(in srgb, var(--error-mark) 22%, transparent); color: var(--error); }
.alert.ok { background: color-mix(in srgb, var(--ok-mark) 8%, transparent); border-color: color-mix(in srgb, var(--ok-mark) 22%, transparent); color: var(--ok); }
.alert.info { background: var(--accent-wash); border-color: var(--accent-tint); color: var(--chip-fg); }
.alert.test-note { background: var(--test-band); border-color: var(--test-band-line); color: var(--test-band-fg); }

/* The test copy's band above the topbar (views/partials/topbar.ejs). */
.test-band {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
    padding: 7px 28px;
    background: var(--test-band); color: var(--test-band-fg);
    border-bottom: 1px solid var(--test-band-line);
    font-size: 13.5px;
}
.test-band strong { font-weight: 700; letter-spacing: .02em; text-transform: uppercase; }
.test-band a { margin-left: auto; color: var(--test-band-fg); font-weight: 600; }

.divider { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 13px; margin: 18px 0; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }

code { font-family: var(--mono); font-size: 12.5px; }
code.block { display: block; padding: 8px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); overflow-x: auto; }

.result, .logview {
    font-family: var(--mono);
    font-size: 12px;
    line-height: 1.55;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 12px 14px;
    margin-top: 14px;
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
}
.logview { max-height: 70vh; overflow-y: auto; white-space: pre; word-break: normal; margin-top: 16px; }
.logview .lvl-error { color: var(--error); }
.logview .lvl-warn { color: var(--warn); }
.logview .lvl-debug { color: var(--muted); }

details summary { cursor: pointer; color: var(--accent); font-size: 13px; font-weight: 500; margin-top: 8px; }
details summary:hover { color: var(--accent-hover); }

/* ── A whole card folded shut ─────────────────────────────────────────────── */
/* Not the accent-coloured "show me the raw JSON" summary above: this one is the
   card's own h2, so a closed card looks like any other card's heading with a
   disclosure triangle beside it, and the space it saves is real. The h2 goes
   inline so the marker, the heading and the one-line answer sit on one line;
   inline also drops its bottom margin, which the summary supplies instead when
   the card is open. */
details.fold > summary { color: var(--text); font-size: inherit; margin: 0; }
details.fold[open] > summary { margin-bottom: 10px; }
/* Shut, a folded card is a row rather than a card with a heading in it, so it
   takes a row's height: the card's own 22px made a stack of four folds twice
   as tall as the design draws them. Opening one puts the space back. */
details.card.fold { padding: 15px 22px; }
details.card.fold[open] { padding: 18px 22px 20px; }
details.fold > summary > h2 { display: inline; margin: 0; }
details.fold > summary:hover > h2 { color: var(--accent); }
details.fold > summary .sub { margin-left: 10px; font-size: 13px; font-weight: 400; color: var(--muted); }
/* The open and close are animated where the browser can (Chromium 131 and up,
   which is what this is read in); everywhere else they snap, which is what
   <details> has always done. */
details.fold { interpolate-size: allow-keywords; }
details.fold::details-content {
    block-size: 0; overflow: clip;
    transition: block-size .2s ease, content-visibility .2s allow-discrete;
}
details.fold[open]::details-content { block-size: auto; }
/* Folded shut outside a card, as a page section: the h2 inside the summary
   gives up its own margins to sit inline with the marker, so the details
   itself carries the space a .section-h2 would have had above it. */
details.fold.section-fold { margin: 26px 0 0; }

/* One prompt version per column, so a change can be read against the one it
   replaced. The colour is the verdict and the hover is the reasoning: a cell
   holding "Yes" says nothing on its own, since whether that is right depends
   on what the person typed two columns to the left. */
.rtw-t td { white-space: nowrap; }
.rtw-t td[title] { cursor: help; }
.rtw-t .rtw-agreed { color: var(--ok); }
.rtw-t .rtw-disagreed { color: var(--error); font-weight: 600; }
.rtw-t .rtw-extra { color: var(--accent); }
.rtw-t .rtw-none { color: var(--muted); }

table.calls { width: 100%; border-collapse: collapse; font-size: 13.5px; margin-top: 10px; }
table.calls th, table.calls td { text-align: left; padding: 8px 12px 8px 0; border-bottom: 1px solid var(--border); }
table.calls th { color: var(--muted); font-weight: 500; }
/* A column head is a label, not a heading: small capitals in the muted
   colour, so the eye goes to the rows. The same voice labels a group. */
table.calls thead th, table.sources thead td, table.values tbody.v-group th, table.calls tbody.t-group > tr > th {
    font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}table.calls td.url { font-family: var(--mono); font-size: 12px; word-break: break-all; }
/* A column of figures, in either table: right aligned and tabular-lined, so it
   can be read down its length rather than one row at a time. */
table.calls th.num, table.calls td.num,
table.records th.num, table.records td.num { text-align: right; font-variant-numeric: tabular-nums; padding-right: 14px; }
/* A figure that is off: the difference column on the OpenAI reconciliation,
   where a day the ledger and the bill disagree on has to be findable by eye
   down a column whose other rows are all zero. Only .status and .tag carried a
   warn variant before, and neither is a table cell. */
table.calls td.warn { color: var(--warn); }

/* ── Probe verdicts ───────────────────────────────────────────────────────── */
.verdicts { list-style: none; margin: 8px 0 0; padding: 0; font-size: 14px; }
.verdict { display: flex; gap: 8px; align-items: baseline; margin: 3px 0; }
.verdict .mark { font-family: var(--mono); width: 1em; flex: none; }
.verdict.ok .mark { color: var(--ok); }
.verdict.error .mark { color: var(--error); }
.verdict.error { color: var(--error); }
.verdict.muted { color: var(--muted); }

.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 8px; background: var(--muted); }
.dot.ok { background: var(--ok-mark); }
.dot.warn { background: var(--warn-mark); }
.dot.error { background: var(--error-mark); }
/* The half of a two-way switch that is not chosen: paler than muted, so the
   row's meaning sits on the half that is. */
.dot.pale { background: var(--border-strong); }
.dot.accent { background: var(--accent); }

.probe-steps .card h2 { display: flex; align-items: center; font-weight: 600; }
.probe-steps .result { max-height: 60vh; overflow-y: auto; }

/* ── A comment, drawn whole ───────────────────────────────────────────────── */
/* Under an opened row on the comment archive, and inside the bundle preview on
   Extraction and a record's page. It keeps a readable measure for the same
   reason `.eb-text` does: a write-up set across an 1850 table is prose in a
   1,500px line. `.card.comment` and its four verdict borders went with the
   card list on 2026-09-09. */
.comment-body { max-width: 100ch; white-space: pre-wrap; word-break: break-word; font-size: 14px; }

/* ── An email's body, paragraph by paragraph ───────────────────────────────── */
/* The whole body is always shown; the filter's verdict is an annotation on it,
   never a cut. A read paragraph looks like ordinary text so that scanning finds
   what a model actually sees; a dropped one is dimmed and named, and keeps its
   left border so a long thread reads as one struck-through block rather than as
   forty separate judgements. Not struck through with a line: these run to
   thousands of characters and line-through makes them unreadable, and being
   unreadable is exactly wrong on the page whose job is checking the rule. */
.email-body { margin-top: 8px; }
.eb { --eb-gutter: 150px; --eb-gap: 8px; display: flex; gap: var(--eb-gap); align-items: baseline; padding: 3px 0 3px 9px; border-left: 2px solid transparent; }
/* A block keeps a readable measure, the same 100 characters a wide page's
   paragraphs keep. It ran the full width of whatever held it until
   2026-09-08, which was tolerable in a 1500 card and is not on an 1850
   table: an email body is prose, and a 1,500px line loses the reader on the
   way back to the start of the next one. */
.eb-text { margin: 0; max-width: 100ch; white-space: pre-wrap; word-break: break-word; font-size: 14px; }
.eb-read { border-left-color: var(--accent); }
.eb-cut { border-left-color: var(--border); opacity: .55; }
.eb-cut .eb-text { font-size: 13px; }
.eb-rule {
    flex: none; width: var(--eb-gutter); text-align: right; padding-top: 1px;
    font-size: 11px; font-weight: 600; color: var(--warn);
}
/* A read paragraph has no rule to name, but it still starts where every other
   paragraph starts. The gutter is empty rather than absent: the body is one
   column of text down the page, and a kept paragraph that began 158px to the
   left of the quoted one under it read as a different document rather than as
   the same email with an annotation beside it. */
.eb-read .eb-text { margin-left: calc(var(--eb-gutter) + var(--eb-gap)); }
/* Under 700px the rule sits on its own line above the text, so there is no
   gutter to line up with and the indent would be most of the screen. */
@media (max-width: 700px) {
    .eb { display: block; }
    .eb-rule { width: auto; text-align: left; display: block; }
    .eb-read .eb-text { margin-left: 0; }
}

/* ── (i) help marks ───────────────────────────────────────────────────────── */
/* A round (i) where the page's prose waits to be asked for. The markup still
   says "?" (in the views and in helpMark in common.js); the glyph is drawn
   here, so every mark on every page changed in one place. */
button.help {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px; padding: 0; margin-left: 7px;
    border: 1px solid var(--border-strong); border-radius: 50%;
    background: var(--surface); color: var(--muted);
    font-family: var(--font); font-size: 0; line-height: 1; letter-spacing: 0; text-transform: none;
    cursor: pointer; vertical-align: 2px; flex: none;
}
button.help::before { content: "i"; font-size: 11px; font-weight: 700; }
button.help:hover { border-color: var(--accent); color: var(--accent); }
.help-pop {
    position: fixed; z-index: 60; max-width: 440px;
    background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
    padding: 12px 14px; font-size: 13px; line-height: 1.55; color: var(--text-2);
    font-weight: 400; font-style: normal; text-align: left;
    box-shadow: 0 8px 28px var(--shadow);
    white-space: pre-line;
}

/* ── Fetcher activity feed ────────────────────────────────────────────────── */
.feed { list-style: none; margin: 10px 0 0; padding: 0; font-size: 13px; max-height: 45vh; overflow-y: auto; }
.feed li { padding: 7px 0; border-bottom: 1px solid var(--border); }
.feed li:last-child { border-bottom: none; }
.feed .when { color: var(--muted); font-variant-numeric: tabular-nums; }
.feed .line { margin: 1px 0 0; }
.feed .line.error { color: var(--error); }

/* ── Expandable analysis rows on /extraction ──────────────────────────────── */
/* A provider heading inside table.calls, on the same reasoning as the field
   groups in table.values: a rule and a muted label rather than a filled band,
   which would compete with the figures the grouping exists to organise. The
   header cell is already muted and ruled by the base th rule above, so all
   this adds is the air that separates one group from the one before it. */
/* The cost table's last five columns are two groups (every source, comments
   only) spanned by a first header row. A rule down the left of each group is
   what separates them: a reader comparing two figures has to see which pair
   belongs together, and the gap alone was not enough at these column widths. */
table.calls th.group-start, table.calls td.group-start { border-left: 1px solid var(--border); padding-left: 14px; }
/* A spanning heading is centred over its columns, overriding the right
   alignment the figures beneath it want: right aligned it sat above the last
   column of the group and read as a label for that one column alone.
   `:not(:last-child)` is what says "a spanning row": a heading row is spanning
   only when a second row of real column headings sits under it. Without it the
   rule reached the single heading row of every other table.calls on the site,
   centring headings over left-aligned columns and taking the rule off under
   them. */
table.calls thead tr:first-child:not(:last-child) th { padding-bottom: 2px; font-size: 12px; border-bottom: none; text-align: center; }
table.calls thead tr:first-child:not(:last-child) th:empty { border: none; }

table.calls tbody.t-group > tr > th { padding-top: 16px; font-size: 12px; font-weight: 600; }
table.calls tbody.t-group:first-of-type > tr > th { padding-top: 6px; }
table.calls tr.expandable { cursor: pointer; }
table.calls tr.expandable:hover td { background: var(--bg); }
table.calls tr.detail > td { padding: 0 0 12px; }
table.calls tr.detail .result { margin-top: 6px; max-height: 50vh; overflow-y: auto; }

/* A run's date, its model and its prompt version: quiet monospace metadata on
   the recent-runs and reconciliation tables, so the eye lands on the
   candidate and the outcome the table is actually scanned for, never on
   which model happened to answer. */
table.calls td.rl-meta { font-family: var(--mono); font-size: 12.5px; color: var(--muted); }

/* ── Flow strips ──────────────────────────────────────────────────────────── */
/* A row of stage boxes joined by arrows: the pipeline on the overview, and the
   filter funnel on the comment archive. A box is a link, a button or a plain
   div depending on the page, so the look is shared and the element is not.

   A grid rather than a wrapping flex row, because the file funnel has eight
   stages and wraps: flex sizes each line on its own, so the three that fell to
   the second line stretched to share the full width and came out half as wide
   again as the five above them. The columns repeat in pairs, a stage then the
   arrow after it, which is exactly the order the children come in, so a line
   always breaks after an arrow and a stage is never dealt an arrow's column. A
   short line simply leaves its trailing columns empty. auto-fit, not auto-fill:
   a strip that fits on one line (the overview's four) collapses the columns it
   does not use and fills the width as it always did. */
/* The floor is what decides when the funnel wraps, and a wrap leaves a dangling
   arrow at the end of the row and one orphaned stage under it. 150px is the
   longest funnel (the email filter's six stages) inside `.page.wide`: six boxes,
   five arrows and eleven gaps come to 1,098px against the 1,152px available.
   The boxes are 1fr above the floor, so a shorter funnel still fills its row. */
.flow {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr) 22px);
    align-items: stretch; gap: 8px; margin: 14px 0 6px;
}
/* The same sum inside `.page.widest`, where the longest funnel is the file
   archive's ten stages: ten boxes, nine arrows and nineteen gaps come to
   1,772px against the 1,802 available, so the floor drops to 140 and the row
   holds all ten. At 150 it fitted nine and left the tenth stranded under a
   dangling arrow, which is the wrap this floor exists to prevent. */
.page.widest .flow { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr) 22px); }
/* A stage is a white panel with its name as a label and its figure in the
   title voice. The top edge carries a stage's kind or state where it has one
   (a funnel's red, amber and accent), and is a plain hairline where it has
   not. */
.flow-stage {
    display: flex; flex-direction: column; gap: 4px; padding: 14px 16px 13px;
    text-align: left; background: var(--surface); border: 1px solid var(--border);
    border-top: 3px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-card);
    cursor: pointer; font: inherit; color: var(--text); text-decoration: none;
    transition: border-color .1s, box-shadow .1s;
}
div.flow-stage { cursor: default; }
.flow-stage:hover { border-color: var(--border-strong); box-shadow: 0 2px 8px var(--shadow); }
div.flow-stage:hover { border-color: var(--border); box-shadow: var(--shadow-card); }
.flow-stage.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.flow-stage.warn { border-top-color: var(--warn-mark); }
.flow-stage.error { border-top-color: var(--error-mark); }
.flow-title { font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-2); }
.flow-n { font-size: 28px; line-height: 1.1; font-weight: 600; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }
.flow-note { line-height: 1.4; }
.flow-arrow { align-self: center; text-align: center; color: var(--muted); font-size: 16px; }
/* A record's own strip: the same four boxes counting one record. A stage
   with nothing to do here (no extractor for this kind, nothing readable)
   goes quiet rather than amber: nobody is needed, it is simply not built. */
.record-flow { margin: 10px 0 16px; }
.flow-stage.quiet { border-top-color: var(--border); color: var(--muted); }
.flow-stage.quiet .flow-n { color: var(--muted); }

/* ── Overview ─────────────────────────────────────────────────────────────── */
/* The pipeline reads as one instrument rather than four boxes on a shelf: a
   single white panel split into four columns by hairlines, a round chevron
   sitting on each divider rather than an arrow in its own grid cell. It does
   not reuse .flow-stage, whose gapped boxes and coloured top border are built
   for a row that can wrap (the record strip above, the comment funnel); this
   strip never wraps, so it keeps its own .ov-* rules instead. Colour is spent
   only where the design language reserves it: the accent on Review the
   moment a proposal is open, since that is the one column that needs a
   person, and otherwise a plain or a warn/error dot and a word for how a
   stage stands. */
.ov-date { margin: 0 0 8px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
/* The one size on this page bigger than the shared h1, scoped to the page's
   own wrapper class so the shared rule above stays untouched for everyone
   else. */
.ov h1 { font-size: 40px; }

.ov-pipeline {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
    /* The four parts of a stage are rows of the strip rather than of a
       column, so each column takes its rows from here (subgrid below) and the
       hairline under the figure falls on one line across all four, whether a
       stage's noun takes one line or two (Kevin, 2026-09-23). */
    grid-template-rows: repeat(4, auto);
    margin: 28px 0 44px; background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-card);
}
.ov-stage {
    position: relative; display: grid; grid-template-rows: subgrid; grid-row: span 4; gap: 16px;
    padding: 24px 26px 22px; color: inherit; text-decoration: none;
    border-right: 1px solid var(--border);
}
.ov-stage:last-child { border-right: 0; }
a.ov-stage:hover { background: var(--bg); }
/* The one column that needs a person: tinted the moment a proposal is open
   and plain the moment the queue is empty, so the wash is the state and not
   a decoration this column always carries. */
.ov-stage.ov-attn { background: var(--accent-wash); }

.ov-stage-head { display: flex; align-items: center; gap: 10px; }
.ov-stage-no { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.ov-stage-label { font-size: 12px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-2); }
.ov-stage-flag { margin-left: auto; font-size: 11px; font-weight: 600; color: var(--accent); }

/* Held to the top of its row, which is as tall as the longest noun in the
   strip: the figures line up with each other and the rule under them is
   straight, rather than each column setting its own height. */
.ov-stage-figure { display: flex; flex-direction: column; align-self: start; gap: 4px; }
.ov-figure { font-size: 46px; line-height: 1; font-weight: 600; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }
.ov-stage-noun { font-size: 14px; color: var(--text-2); }

.ov-stage-rule { height: 1px; background: var(--border); }

.ov-stage-state { display: flex; flex-direction: column; gap: 3px; font-size: 13px; }
.ov-state-line { display: flex; align-items: baseline; }
.ov-state-line .dot { flex: none; margin-right: 8px; transform: translateY(-1px); }
.ov-state-sub { color: var(--muted); }

/* A round badge standing on the hairline between two columns, in place of the
   arrow the shared .flow strip draws in a grid cell of its own: there is no
   column to spare for one here. */
.ov-chevron {
    position: absolute; top: 50%; right: -13px; margin-top: -13px;
    width: 26px; height: 26px; border-radius: 50%;
    background: var(--bg); border: 1px solid var(--border); color: var(--muted);
    display: flex; align-items: center; justify-content: center; z-index: 1;
}

/* ── Overview: the System cards ───────────────────────────────────────────── */
/* The strip above it already ends in air, so this section heading needs less
   of it than the page frame's own. */
.ov .sec { margin-top: 0; }
.ov-card { display: flex; flex-direction: column; gap: 14px; }
.ov-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.ov-card-head h2 { margin: 0; }
.ov-card-head .status { margin: 0; }
.ov-card-sub { font-size: 12px; color: var(--muted); }
/* An email address has nowhere to break, so at this card's width it ran past
   the edge rather than wrapping under its own label. */
.ov-card .kv dd { min-width: 0; overflow-wrap: anywhere; }

.ov-quota-row { display: flex; align-items: baseline; gap: 10px; }
.ov-quota-figure { font-size: 34px; line-height: 1; font-weight: 600; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }
.ov-quota-of { font-size: 13px; color: var(--text-2); }

/* A <progress> rather than a div sized by an inline width, which the content
   security policy refuses: the same reason progress.job-bar exists on
   /jobs. Coloured by how the day's quota stands, the same three states as
   everywhere else, so the fill is never the only thing saying so; the figure
   and "of N allowed" beside it are the word that goes with it. */
/* ── Who decides: the fields page behind Review's switch ──────────────────── */
/* The two guards sit side by side with the tick leading, as the design draws
   them: they are two facts about every field on the page, not two rows of a
   list to read down. */
.guards { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px 32px; }
.guards .sw-row { grid-template-columns: auto minmax(0, 1fr); align-items: start; padding: 0; border-top: 0; }
.guards .sw-tick { order: -1; }
/* How a field has been decided: a short bar with the figures beside it, not a
   bar across the column. Green, because what it measures is the share that
   went through, and the figures beside it are the words that go with the
   colour. */
progress.dec-bar {
    width: 90px; height: 6px; appearance: none; border: 0; border-radius: 3px;
    overflow: hidden; background: var(--border); color: var(--ok-mark);
    vertical-align: middle; margin-right: 8px;
}
progress.dec-bar::-webkit-progress-bar { background: var(--border); border-radius: 3px; }
progress.dec-bar::-webkit-progress-value { background: var(--ok-mark); border-radius: 3px; }
progress.dec-bar::-moz-progress-bar { background: var(--ok-mark); border-radius: 3px; }

/* The feedback picks, on /extraction: a bar a row rather than green-means-
   good, since what it counts is neither, a reviewer's reason for calling a
   proposal wrong. */
.fb-picks { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 16px; }
.fb-pick-row { display: grid; grid-template-columns: 180px minmax(0, 1fr) 40px; align-items: center; gap: 10px; font-size: 13px; }
.fb-pick-row .dec-bar { width: 100%; color: var(--muted); margin-right: 0; }
.fb-pick-row .dec-bar::-webkit-progress-value { background: var(--muted); }
.fb-pick-row .dec-bar::-moz-progress-bar { background: var(--muted); }
.fb-pick-label { color: var(--text-2); }
/* The group's own line: how many fields it holds and how they stand, beside
   the name. */
.t-group .g-note { margin-left: 10px; font-weight: 400; font-size: 12.5px; color: var(--muted); text-transform: none; letter-spacing: 0; }
/* On Who decides a group is a subject flipped together, so its name is a
   title rather than a label: dark, bold and life-sized, with room above it
   to part it from the group before (Kevin, 2026-09-29). The space is what
   parts them: a faint grey panel per group was tried the same day and taken
   out again. The switch cells beside the name take the same padding, or
   they sit off the name's line. */
table.calls.who-decides tbody.t-group > tr:first-child > th {
    font-size: 14.5px; font-weight: 700; letter-spacing: 0; text-transform: none; color: var(--text);
}
table.calls.who-decides tbody.t-group > tr:first-child > * { padding-top: 44px; padding-bottom: 10px; }
table.calls.who-decides tbody.t-group:first-of-type > tr:first-child > * { padding-top: 12px; }

progress.ov-quota-bar {
    width: 100%; height: 6px; appearance: none; border: 0; border-radius: 3px;
    overflow: hidden; background: var(--border); color: var(--ok-mark);
}
progress.ov-quota-bar::-webkit-progress-bar { background: var(--border); border-radius: 3px; }
progress.ov-quota-bar::-webkit-progress-value { background: var(--ok-mark); border-radius: 3px; }
progress.ov-quota-bar::-moz-progress-bar { background: var(--ok-mark); border-radius: 3px; }
progress.ov-quota-bar.warn { color: var(--warn-mark); }
progress.ov-quota-bar.warn::-webkit-progress-value { background: var(--warn-mark); }
progress.ov-quota-bar.warn::-moz-progress-bar { background: var(--warn-mark); }
progress.ov-quota-bar.error { color: var(--error-mark); }
progress.ov-quota-bar.error::-webkit-progress-value { background: var(--error-mark); }
progress.ov-quota-bar.error::-moz-progress-bar { background: var(--error-mark); }

.ov-note { margin: 0; font-size: 12.5px; color: var(--muted); }

.ov-providers { display: flex; flex-direction: column; }
.ov-provider { display: flex; align-items: center; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
.ov-provider:last-child { border-bottom: 0; padding-bottom: 0; }
.ov-provider-name { flex: 1; font-weight: 500; }
.ov-provider .status { margin: 0; }

/* The name, the id in the mono voice ids and dates share, and whose record
   it is pushed to the far end of the same line. Baseline-aligned rather than
   centred, so the recruiter's name sits on the name's own line and not
   stretched to the middle of a heading; it wraps under only on a narrow
   window, which is where a heading and a name cannot share a line honestly.
   Italic where nobody owns the record, so an absence does not read as
   somebody's name. */
.record-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px 20px; flex-wrap: wrap; }
.record-head h1 { font-size: 34px; }
.record-id { margin-left: 12px; font-family: var(--mono); font-size: 14px; font-weight: 400; color: var(--muted); vertical-align: 2px; }
.record-owner { color: var(--muted); font-size: 14px; white-space: nowrap; }
.record-owner.none { font-style: italic; }

/* Where a record stands with the extractor, under its name: the state as
   the same pill every status reads on this app (a dot and a word, tinted by
   tone), then the sentence that explains it, which stays body text because
   it is the half that is actually read. The pill and, for an admin, the
   "Read now" action share one bordered bar, so a person reads what stands
   and can act on it in the same glance rather than two lines apart. */
.record-status-bar {
    display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
    margin: 12px 0 16px; padding: 13px 18px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 12px; box-shadow: var(--shadow-card);
}
.record-status { flex: 1; min-width: 240px; }
.record-status p { margin: 0 0 3px; }
.record-status-line { font-size: 14px; }
.record-status-line .status { margin: 0 2px 0 0; vertical-align: -1px; }
.record-run-actions { display: flex; align-items: center; gap: 8px; flex: none; }

/* The CRM's values are two narrow columns of eleven rows, so the table keeps
   its own width inside a full-width section rather than stretching its labels
   half a screen away from the numbers they name. */
/* The CRM's values, and beside them the photograph it holds. The picture is
   not one of the extractor's fields and never will be, no model reading it
   and none proposing it, but it is the one thing on the card a reader takes
   in without reading, and whether there is one at all decides whether a
   headshot found in a CV ever reaches Review. The card widens by exactly the
   picture and its gap, so the fields keep the width they were set for.

   Where the CRM holds none, a dashed frame reads "empty", the same word and
   the same muted grey a field with nothing in it uses two columns away: the
   caption is the label and stays put, the frame is the value and varies. An
   absence stated is read; an absence left blank is missed, and this is a card
   whose whole job is to say what is there. */
.record-held { display: flex; align-items: flex-start; gap: 16px; max-width: 756px; }
.record-held > .values { flex: 1; }
.held-photo { flex: none; width: 120px; margin: 0; text-align: center; }
.held-photo-img { display: block; max-width: 120px; max-height: 150px; margin: 0 auto; border: 1px solid var(--border); border-radius: 6px; }
.held-photo-img.empty { display: flex; width: 120px; height: 150px; align-items: center; justify-content: center; border-style: dashed; color: var(--muted); font-size: 13px; }
.held-photo figcaption { margin-top: 6px; }

/* ── A record's archive: one table over everything collected ──────────────── */
/* Read and rejected items in one date order, because a rule can only be judged
   against the items beside it. A rejected row is struck through in red where
   it stands: it is one line, so a line through it stays readable, which is
   why this and the paragraph-level `.eb-cut` (thousands of characters, dimmed
   instead) do not draw rejection the same way.

   The two character columns are the point of the table, so they are tabular
   and right aligned, and the row's own state never touches them: a struck-out
   number is unreadable and the size of what was thrown away is exactly what a
   person is here to see. */
table.archive { table-layout: fixed; }
table.archive th { font-weight: 500; }
table.archive td, table.archive th { padding-right: 12px; }
table.archive .ai-when { width: 14em; white-space: nowrap; font-family: var(--mono); font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
table.archive .ai-kind { width: 8em; white-space: nowrap; }
/* The headshot found in a file, beside its kind tag: a thumbnail one line
   tall, the picture at full size while the pointer rests on it. The preview
   takes no pointer of its own, as on the file archive, or it would sit
   between the cursor and the rows it covers. The edge says what became of
   it: green once written, red once rejected, plain otherwise. */
.ai-shot { position: relative; display: inline-block; vertical-align: middle; margin-left: 8px; }
.ai-shot-thumb { display: block; height: 22px; width: 18px; object-fit: cover; border: 1px solid var(--border); border-radius: 3px; }
.ai-shot.approved .ai-shot-thumb { border-color: var(--ok); }
.ai-shot.rejected .ai-shot-thumb { border-color: var(--error); opacity: .6; }
.ai-shot-full { display: none; pointer-events: none; position: absolute; left: 0; top: 100%; z-index: 5; width: 200px; height: auto; border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 8px 24px var(--shadow); }
.ai-shot:hover .ai-shot-full { display: block; }
.ai-shot:hover .ai-shot-thumb { border-color: var(--accent); }
table.archive .ai-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
table.archive th.num, table.archive td.num { width: 8.5em; }
table.archive .ai-status { width: 17em; color: var(--muted); }
table.archive .tag { margin-left: 0; }

/* The strike lands on the date and the origin only. text-decoration cannot be
   cancelled by a descendant, so putting it on the row would have drawn a line
   through the kind tag and, worse, through the two numbers, which are the
   whole reason a rejected row is on screen at all. */
table.archive tr.ai-stripped td, table.archive tr.ai-cut td { color: var(--muted); }
table.archive tr.ai-stripped .ai-when, table.archive tr.ai-stripped .ai-what { text-decoration: line-through; text-decoration-color: var(--error); }
table.archive tr.ai-cut .ai-when, table.archive tr.ai-cut .ai-what { text-decoration: line-through; text-decoration-color: var(--warn); }
table.archive tr.ai-stripped .ai-status { color: var(--error); }
table.archive tr.ai-cut .ai-status { color: var(--warn); }
table.archive tr.ai-read .ai-n-read { color: var(--ok); }
/* The open row sinks towards --bg, the same way the hover on every table.calls
   inside a card does: this table is in a card too, so one rule covers both. */
table.archive tr.open td { background: var(--bg); }

/* The totals are the answer to "how much of this does the AI actually read?",
   so they sit under the columns they add up rather than in a sentence
   somewhere. Heavier than the rows and ruled off from them. */
table.archive tfoot th {
    text-align: left; padding: 8px 12px 8px 0; font-weight: 600;
    border-top: 1px solid var(--border); border-bottom: none;
}
table.archive tfoot th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.archive tfoot th.muted { font-weight: 500; }

/* An opened row: what the rule said, then the item itself. */
table.archive tr.detail > td { padding: 4px 0 16px; }
.ai-verdict { margin: 0 0 6px; font-size: 13px; color: var(--muted); }
.ai-verdict b { color: var(--error); }

/* ── Comments: the extraction filter ────────────────────────────────────────── */
/* The funnel's boxes are buttons that preset the kind filter below, so it is
   not an illustration but a control over the same data. Red for what Vincere
   wrote, amber for picks, muted grey for house notes: dropping those was a
   decision someone made, so there is nothing there to act on. */
.cf-stage-all { border-top-color: var(--muted); }
.cf-stage-system { border-top-color: var(--error); }
.cf-stage-picklist { border-top-color: var(--warn); }
.cf-stage-house { border-top-color: var(--muted); }
.cf-stage-extract { border-top-color: var(--accent); }

/* Rule tiles. Sized so the whole rule set fits on one screen without scrolling,
   because comparing them against each other is the point of the page. */
.cf-rules { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); gap: 8px; margin: 12px 0 4px; }
.cf-rule {
    display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; text-align: left;
    background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--muted);
    border-radius: var(--radius); cursor: pointer; font: inherit; color: var(--text);
}
.cf-rule:hover { border-color: var(--accent); }
.cf-rule.active { border-color: var(--accent); border-left-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.cf-rule-extract { border-left-color: var(--accent); }
.cf-rule-system { border-left-color: var(--error); }
.cf-rule-picklist { border-left-color: var(--warn); }
.cf-rule-house { border-left-color: var(--muted); }
.cf-rule-n { font-size: 21px; font-weight: 650; font-variant-numeric: tabular-nums; }
.cf-rule-label { font-size: 13px; font-weight: 600; }
.cf-rule-note { line-height: 1.35; }
.cf-rule-share { font-variant-numeric: tabular-nums; }

/* Per-comment badges. The left border repeats the verdict so a long scroll
   stays readable without reading each badge. Accent for "kept", not green:
   green would say verified-good, and kept only means unrecognised. */
.cf-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 0 0 7px; }
/* A tag is a quiet chip: the grey fill and the text colour, never an outline
   in a colour (the redesign of 2026-09-22). One that carries a state takes
   that state's tint and darker word, the same pair a .status pill uses. */
.tag, .cf-tag {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 1px 8px; border-radius: 6px; border: 1px solid transparent;
    background: var(--chip); color: var(--chip-fg);
    font-size: 11.5px; font-weight: 500; line-height: 1.5;
}
.tag { margin-left: 2px; vertical-align: 1px; white-space: nowrap; }
/* Only for something a person should act on: a paused or failing fetcher is
   otherwise invisible while its system block is closed. */
.tag.warn, .cf-tag-picklist { background: color-mix(in srgb, var(--warn-mark) 13%, transparent); color: var(--warn); }
.tag.ok { background: color-mix(in srgb, var(--ok-mark) 11%, transparent); color: var(--ok); }
.tag.error, .cf-tag-system { background: color-mix(in srgb, var(--error-mark) 11%, transparent); color: var(--error); }
.cf-tag-extract { background: var(--accent-tint); color: var(--accent-hover); }
.cf-tag-house { background: var(--chip); color: var(--muted); }
/* An archive browser is a table, one item a row and one fact a column, so the
   list scans down a column for the same thing (Kevin, 2026-09-05: the file
   archive was a card per document, three lines each, and the same fact sat
   somewhere different on every card; a table is also half the height). The
   file archive went first; the email archive followed on 2026-09-08 for the
   same reasons, so the shared columns are styled once here under
   `table.browse` and each page adds only the columns that are its own.

   Every column but the title's is a fixed width and clips with the whole
   value on hover, so the file name or the subject takes what is left. The two
   character counts sit in one cell as read over held, the read count green,
   because the gap between them is what the column is for. The headshot, which
   only files have, is a thumbnail two lines tall with its score beside it;
   the full-size picture is the same bytes, shown over the table while the
   pointer rests on the thumbnail, so the row stays short and checking a find
   is a hover rather than a tab. */
/* The list is a card, like every other section of the app, so the table has
   an edge and a floor instead of floating on the page's own colour (Kevin,
   2026-09-05). Tighter than a card of prose, the rows carrying their own
   padding. The headings follow the scroll, because the whole archive is one
   table and column headings that scroll away are no use by row 300; they sit
   on the card's colour, and their rule is a shadow rather than a border,
   since a collapsed border does not travel with a sticky cell. */
.cf-list { padding: 2px 16px 6px; }
.cf-list table.calls { margin-top: 0; }
table.browse thead th { position: sticky; top: 0; z-index: 2; background: var(--surface); border-bottom: none; box-shadow: inset 0 -1px 0 var(--border); }
table.browse td { vertical-align: middle; }
table.browse .cf-verdict { width: 14em; white-space: nowrap; }
table.browse .cf-type, table.browse .cf-who, table.browse .cf-from,
table.browse .cf-by, table.browse .cf-act { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The document name, or an email's subject, wraps rather than being cut at the
   column's edge: it is what a row is found by, and the ellipsis fell in the
   middle of the name a person was scanning for. Two lines at most, so a row is
   still a row and the column keeps its rhythm, with the ellipsis after them and
   the whole name on hover as before. The break may land inside a word, because
   a file name is often one long word with underscores where the spaces would be
   (Kevin, 2026-09-08). */
table.browse .cf-title { white-space: normal; }
table.browse .cf-title > span {
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; line-clamp: 2;
    overflow: hidden; overflow-wrap: anywhere;
}
/* A name that is one machine-made token: break on the character the line ends
   on. overflow-wrap only breaks inside a word when there is nowhere else to
   break, so the hyphen in "1715337391037-e=1772064000&v=beta&t=..." was taken
   first and left most of the line empty with the rest still not fitting on
   the next. The page script marks these rows, since break-all would split an
   ordinary word as readily as a hash (Kevin, 2026-09-08). */
table.browse .cf-title > span.cf-dense { word-break: break-all; }
table.browse .cf-title .cf-orig { font-weight: 650; cursor: help; }
/* The comment archive's is the same column and takes what is left the same
   way, but on one line. A file name and a subject are titles, and cutting one
   loses the word being scanned for, which is why they wrap to two; a comment
   is prose, so two lines of it is not a title but the opening of a paragraph.
   It made almost every row twice as tall for a preview nobody reads to the
   end of (Kevin, 2026-09-09). One line, clipped like every other fixed
   column, with the opening on hover and the whole thing one click away. */
table.browse .cf-line { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
table.browse .cf-type { width: 10em; }
/* Wide enough for the column head itself ("Format"), the narrowest thing
   ever in it: a fixed table takes its width from the header row, at the
   header's own 11px, so the word is the constraint rather than "docx". */
table.browse .cf-ext { width: 6.5em; }
/* Wide enough for a full date and time ("22 Sept 2026, 14:07"), which the
   9em this replaced was not: a fixed table sizes the column from the
   header cell at 11px, where 9em is 99px, well under what the date needs,
   so the text ran on into the next column rather than wrapping or
   clipping. Ids, dates and provenance read in the mono face; the rule
   sits on the data cell alone, `table.browse td.cf-when` below, so the
   column head keeps the small-capitals every other head is set in. */
table.browse .cf-when { width: 16em; white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums; }
table.browse td.cf-when { font-family: var(--mono); font-size: 12px; }
table.browse th.num, table.browse td.num { width: 8.5em; white-space: nowrap; }
table.browse .cf-read { color: var(--ok); }
table.browse .cf-kind { width: 6.5em; color: var(--muted); }
table.browse .cf-who { width: 19em; }
table.browse .cf-shot { width: 5.5em; white-space: nowrap; }
table.browse .cf-shot .ph { position: relative; display: inline-block; vertical-align: middle; margin-right: 6px; }
table.browse .ph-thumb { display: block; height: 26px; width: auto; max-width: 26px; object-fit: cover; border: 1px solid var(--border); border-radius: 4px; }
/* The preview takes no pointer of its own: it hangs over the rows below
   the thumbnail, and while it could be hovered it kept itself alive under
   the cursor and swallowed the click meant for the next row (Kevin,
   2026-09-05). Untouchable, it is gone the moment the pointer leaves the
   thumbnail, and everything under it stays reachable. */
/* --shadow, the popover's own token, not --bg: on a dark page a near-black
   --bg read as a shadow, but on this light one it is a pale panel colour and
   the picture floated with no shadow at all under it (Kevin's redesign,
   2026-09-22). */
table.browse .ph-full { display: none; pointer-events: none; position: absolute; right: 0; top: 100%; z-index: 5; max-width: none; max-height: 360px; border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 8px 24px var(--shadow); }
table.browse .ph:hover .ph-full { display: block; }
table.browse .ph:hover .ph-thumb { border-color: var(--accent); }
/* The score carries the words, so hovering the picture shows the picture
   and nothing else. cursor: help says the number is worth resting on, as on
   the review card's photo and its basis chip. */
table.browse .ph-score { font-size: 12px; font-variant-numeric: tabular-nums; cursor: help; }
/* The email archive's own two columns. The sender is an address, so it clips
   with the whole of it on hover like every other fixed column; the direction
   is one word and sits muted beside it, because which way a mail went is
   context for the sender rather than a fact to scan for. */
table.browse .cf-from { width: 20em; }
/* Wide enough for the column head ("Direction"), which at 5.5em ran into
   the head beside it; "received" and "sent" needed far less. */
table.browse .cf-dir { width: 7.5em; color: var(--muted); }
/* The comment archive's own two. The recruiter and the KPI action a comment was
   logged under were two of five facts on a meta line; as columns they are
   scannable, which is what the line never was. A third, a green mark on any
   comment matching the audit's keyword test, went on 2026-09-09: it lit up on
   45% of the archive, and the banner already names the comments that matter. */
table.browse .cf-by { width: 12em; }
table.browse .cf-act { width: 9em; color: var(--muted); }

/* ── Extraction: the next analysis, laid out before the button ───────────── */
/* The write-up on the left, the CRM's current values on the right: the two
   things the model is shown, side by side, so "what will it read?" needs no
   scrolling. Below 900px they stack. */
.sub-h { margin: 14px 0 6px; font-size: 13px; font-weight: 600; color: var(--muted); }
.sub-h button.help { vertical-align: 0; }
label.inline { display: inline-flex; align-items: center; gap: 6px; margin: 0; }
.preview { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 20px; margin-top: 6px; }
@media (max-width: 900px) { .preview { grid-template-columns: minmax(0, 1fr); } }
.write-up { padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 7px; max-height: 50vh; overflow-y: auto; }
table.values { width: 100%; border-collapse: collapse; font-size: 13px; }
table.values td { padding: 3px 8px 3px 0; border-bottom: 1px solid var(--border); }
table.values td.v { text-align: right; font-variant-numeric: tabular-nums; }
table.values td.v.empty { color: var(--muted); }
/* One tbody per subject (compensation, availability), because thirteen fields
   in one run read as a wall. The heading is the only thing separating them:
   a rule above and the muted weight of a label, not a filled band, which would
   compete with the values it is there to organise. */
table.values tbody.v-group th {
    padding: 12px 0 3px; text-align: left; font-size: 12px; font-weight: 600;
    color: var(--muted); border-bottom: 1px solid var(--border);
}
table.values tbody.v-group:first-child th { padding-top: 2px; }

/* The bundle, one row per item: date, kind, size, then who or what. The sizes
   are the column a reader scans, so they are tabular and right aligned with a
   unit of fixed width behind them, which keeps the digits in line. The last
   column takes the slack and truncates: a long CV filename must not push the
   numbers around. */
table.bundle-list { width: 100%; border-collapse: collapse; font-size: 13px; }
table.bundle-list td { padding: 3px 10px 3px 0; vertical-align: baseline; }
table.bundle-list td:last-child { padding-right: 0; }
.bi-date { white-space: nowrap; font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.bi-chars { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.bi-what { width: 100%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
/* Which fields an archived item was cited for, on the details strip's bundle
   popover: a small chip per field, the same pill a value's kind label uses. */
.bi-cited { display: flex; flex-wrap: wrap; gap: 3px; }
.cite { display: inline-flex; padding: 1px 7px; border-radius: 6px; background: var(--accent-tint); color: var(--accent-hover); font-size: 11px; font-weight: 600; white-space: nowrap; }
.queue { list-style: none; margin: 4px 0 0; padding: 0; font-size: 13px; }
.queue li { padding: 3px 0; }

/* ── Records: the directory ──────────────────────────────────────────────── */
/* One table per kind of record on the same sheet the tabs open into. The
   counts are right-aligned tabular figures so a column of them scans; the
   name is the only link, kept in text colour rather than the accent blue a
   plain `a` draws elsewhere, since a whole column of blue names would
   compete with the state, which carries the colour here and never stands
   for it alone: a dot beside the word, as everywhere else on the app. */
.records-body { padding: 4px 18px 14px; }
/* The filter row above a panel already keeps its distance. */
.toolbar + .system { margin-top: 0; }
/* The tabs' panel sits close under the tray; the row of filters between
   them is what a person reaches for first. */
[role="tabpanel"] > .toolbar:first-child { margin-top: 18px; }
.records-body [data-filter] { min-width: 220px; }
.records-scroll { overflow-x: auto; }
/* Two and a half thousand rows are scanned across as much as down, so the
   row under the pointer is picked out and the headings stay put, the way
   the archive browsers' do. */
table.records tbody tr:hover td { background: var(--bg); }
table.records thead th { position: sticky; top: 0; z-index: 2; background: var(--surface); border-bottom: none; box-shadow: inset 0 -1px 0 var(--border); }
table.records a { color: var(--text); text-decoration: none; }
table.records a:hover { color: var(--accent); text-decoration: underline; }
table.records .rec-id { font-family: var(--mono); font-size: 12px; }
table.records td.date { white-space: nowrap; font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
/* The recruiter who owns the record. Muted like the dates: it is what the
   table is filtered and scanned by, never what it is read for, and a column
   of names in body colour would compete with the names in the first column
   that are the point of the row. */
table.records td.owner { white-space: nowrap; color: var(--muted); }
table.records td.state { white-space: nowrap; color: var(--text-2); }
table.records td.state .dot { vertical-align: -1px; }
table.records td.state.muted { color: var(--muted); }
/* Why a record is due, a shade under the state itself: it is read once the
   eye has stopped on a row, never scanned down the column. */
.st-why { margin-left: 6px; color: var(--muted); }
.outcome-line { margin: 10px 0 2px; }
.record + .record { margin-top: 10px; padding-top: 6px; border-top: 1px solid var(--border); }
table.changes td em { font-style: italic; color: var(--text); }
table.calls tr.detail .preview { margin-top: 12px; }

/* ── Review: one row per candidate, one open at a time (design D) ─────────── */
/* Closed, a row is the name, owner, date and a count pill plus chips for the
   fields the current filter shows, with a right-hand summary ("2 by Ema, 1 by
   Kevin", "3 auto-accepted"). Open, the row keeps its place (`.open-row`) and
   the body under it draws every field the extractor owns as a tree under its
   Vincere panel: a blue "Need you" zone, a green "Auto-accepted" zone, or, in
   All fields, one list of everything with a tint per state. */
.rv-prompt { margin: 22px 0 6px; }
.rv-prompt:first-child { margin-top: 6px; }
/* A section label, not a heading: small quiet capitals like a column head,
   since the prompt version is the queue's own bookkeeping and every row
   under it is what carries the weight. The current prompt reads a shade
   darker than an older one, both still quiet. */
.rv-prompt h2 {
    display: flex; align-items: center; gap: 8px; margin: 0;
    font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.rv-prompt.current h2 { color: var(--text-2); }
/* One panel per prompt version, the candidate rows inside it on hairlines.
   Each was a bordered card with a gap under it until 2026-09-22, which gave
   a queue of sixty proposals sixty boxes and no edge to scan down. */
/* `.rv-list` rather than `.rv-group`, which the field table's group heading
   row has taken since long before this panel existed: a `tr` wearing a
   panel's border drew a box around "Availability" inside every open card. */
.rv-list { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-card); margin: 8px 0 0; }
.rv-candidate + .rv-candidate { border-top: 1px solid var(--border); }
/* The head is a grid, so every row's name, owner, date and fields start at
   the same place down the list. Name and owner are narrower than a plain
   flex row gave them, so the fields column, which is what the list is read
   for, takes the rest; the right column is fixed, for the written/approved
   summary. */
.rv-head {
    display: grid;
    grid-template-columns: 14px 210px 140px 64px minmax(0, 1fr) minmax(170px, max-content);
    align-items: center; gap: 4px 16px; width: 100%; padding: 11px 18px;
    background: none; border: 0; font: inherit; color: var(--text);
    text-align: left; cursor: pointer;
}
.rv-head:hover { background: var(--bg); }
/* The open row's head keeps its wash under the pointer: it is the row being
   worked on, not a row being aimed at. */
.rv-candidate.open-row > .rv-head:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
/* The open row's name takes a step up, the way a title does, without leaving
   the column the list is scanned down. */
.rv-candidate.open-row .rv-name { font-size: 17px; }
.rv-candidate.open-row .rv-caret { color: var(--accent); }
.rv-caret { width: 14px; color: var(--muted); font-size: 11px; }
/* The name and the id are one cell: they are read as one thing, and the id
   should sit against the name rather than at a column stop of its own. */
.rv-who { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.rv-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; font-size: 15px; }
/* An id, so it takes the mono voice every id and date on the app does. */
.rv-id { flex: none; white-space: nowrap; font-family: var(--mono); color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }
/* Whose candidate this is in Vincere, in a column of its own and as quiet as
   the date: the name is looked for when a reviewer is working through one
   colleague's people, and must never compete with the candidate's own name.
   Italic where there is no owner, so an absence does not read as somebody's
   name. */
.rv-owner { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); font-size: 13px; }
.rv-owner.none { color: var(--muted); }
.rv-owner.none { font-style: italic; }
/* Quieter than the id beside it: the age answers a question the eye asks of
   the whole list at once, and never competes with the name. A date, in the
   same mono voice as the id, and its hover says which date this filter means
   (1.5): "Read on …" under Open, "Latest decision …" under Approved and
   Rejected. */
.rv-when { white-space: nowrap; font-family: var(--mono); color: var(--muted); font-size: 12px; cursor: help; }
/* The fields column: the blue count pill (only under Open, where it is the
   same number as the chips beside it), then the chips the current filter
   shows, clipped to one line with a "+N more" for the rest. */
.rv-fields { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; }
.rv-need-pill {
    flex: none; display: inline-flex; align-items: center; justify-content: center;
    min-width: 24px; height: 24px; padding: 0 8px; box-sizing: border-box;
    border-radius: 999px; background: var(--accent); color: var(--btn-primary-fg);
    font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.rv-chips { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; min-width: 0; overflow: hidden; }
.rv-more { flex: none; align-self: center; padding: 3px 10px; border-radius: 999px; background: var(--accent-tint); color: var(--accent-hover); font-size: 12.5px; font-weight: 700; white-space: nowrap; cursor: help; }
/* A chip is quiet grey, like any other tag on the app, with its kind mark
   leading: a green dot for a fill, an amber dot for an overrule, the navy F
   for a replace, nothing at all for an item too old to carry a kind. A
   dashed edge means inferred, cutting across all three; the small link icon
   means the field is held back with another one. */
.rv-chip {
    display: inline-flex; flex: none; align-items: center; gap: 6px;
    padding: 3px 10px; border-radius: 999px; border: 1px solid transparent;
    background: var(--chip); color: var(--chip-fg);
    font-size: 12.5px; font-weight: 500; white-space: nowrap;
}
.rv-chip.inferred { border: 1px dashed var(--muted); }
.rv-chip .dot { width: 7px; height: 7px; margin: 0; }
.rv-chip .rv-link { color: var(--accent); }
/* The navy "F" for "replaces Fieldwork's own value": a square mark rather
   than a dot, since the fact it carries (a tool wrote this, not a colleague)
   is worth more than a colour. Reused on a held value and on a field row's
   kind label, always the same shape. */
.rv-fw {
    display: inline-flex; flex: none; align-items: center; justify-content: center;
    width: 13px; height: 13px; border-radius: 3px;
    background: var(--fw-mark); color: var(--fw-mark-fg);
    font-size: 9px; font-weight: 700; line-height: 1; cursor: help;
}
/* A dashed pill outline rather than the filled chip every other .tag is:
   this one is a caution about reading, not a state with a colour of its
   own, so it stays grey and quiet whichever field it sits beside. */
.tag.rv-basis {
    padding: 1px 9px; border-radius: 999px;
    background: none; border: 1px dashed color-mix(in srgb, var(--muted) 55%, transparent);
    color: var(--text-2); font-weight: 600; cursor: help;
}
/* The right column: what has already gone through, on Open and Approved
   alike, right aligned like the pill it answers. */
.rv-done { display: flex; align-items: center; justify-content: flex-end; gap: 6px 14px; flex-wrap: nowrap; font-size: 13px; white-space: nowrap; }
.rv-done .rv-by-you { display: inline-flex; align-items: center; gap: 5px; color: var(--text); font-weight: 600; }
.rv-done .rv-by-auto { display: inline-flex; align-items: center; gap: 5px; color: var(--ok); font-weight: 600; }
.rv-done .rv-by-off { color: var(--error); font-weight: 600; }
.rv-done .rv-by-auto svg, .rv-done .rv-by-you svg { flex: none; }

/* The body keeps the row's own surface, the same as the head above it, so an
   open candidate is one framed thing with a rule across it. */
.rv-body { border-top: 1px solid var(--border); border-radius: 0 0 8.5px 8.5px; }
@media (max-width: 900px) {
    .rv-body { overflow-x: auto; }
}

/* The details strip: candidate id, read date, model, prompt version, cost,
   the archived-item count, and "Their page" at the far end. A row of
   labelled cells rather than a sentence, because these six are looked up
   one at a time rather than read in order. */
.rv-strip { display: flex; flex-wrap: wrap; align-items: stretch; padding: 0 18px; background: var(--bg); border-bottom: 1px solid var(--border); }
.rv-strip-cell { display: flex; flex-direction: column; gap: 2px; padding: 9px 22px 9px 0; margin-right: 22px; border-right: 1px solid var(--border); }
.rv-strip-cell:last-of-type { border-right: 0; margin-right: 0; }
.rv-strip-cell .cap { font-size: 10.5px; }
.rv-strip-val { font-size: 13.5px; color: var(--text); }
.rv-strip-end { margin-left: auto; display: flex; align-items: center; }
.rv-page { white-space: nowrap; color: var(--accent-hover); text-decoration: none; font-size: 13.5px; }
.rv-page:hover { text-decoration: underline; }
/* The item count opens what the run read: a small tinted button in the
   accent, as the boards draw it, rather than a dotted underline. */
.rv-items {
    display: inline-flex; align-items: center; gap: 6px; padding: 1px 8px; border-radius: 6px;
    background: var(--accent-tint); color: var(--accent-hover); font-weight: 600; cursor: pointer;
}
.rv-items-caret { font-size: 10px; }
.rv-pop table.bundle-list { margin-top: 4px; }
/* Model notes, 16px above and below, on their own line under the strip. */
.rv-notes { margin: 0; padding: 16px 18px; font-size: 13.5px; color: var(--text-2); border-bottom: 1px solid var(--border); }
.rv-notes b { color: var(--text); }

/* The two zones. Need you is the accent, the same blue that means "needs a
   person" everywhere else; Auto-accepted is green, "already done". Both
   tints are quieter than the open row's own frame, so the frame still reads
   as the outermost thing. */
.rv-zone-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 11px 18px; }
.rv-zone-head h3 { margin: 0; font-size: 16px; font-weight: 600; }
.rv-zone.need .rv-zone-head { background: color-mix(in srgb, var(--accent) 8%, transparent); border-bottom: 1px solid var(--tint-need-line); }
.rv-zone.need .rv-zone-head h3 { color: var(--accent-hover); }
.rv-zone.need .rv-need-pill { margin: 0; }
.rv-zone.auto { background: color-mix(in srgb, var(--ok-mark) 4%, transparent); border-top: 1px solid var(--tint-ok-line); }
.rv-zone.auto .rv-zone-head { background: color-mix(in srgb, var(--ok-mark) 10%, transparent); border-bottom: 1px solid var(--tint-ok-line); }
.rv-zone.auto .rv-frow + .rv-frow { border-top-color: var(--tint-ok-line); }
.rv-zone.auto .rv-zone-head h3 { color: var(--ok); }
.rv-zone.auto .rv-zone-head svg { color: var(--ok-mark); flex: none; }
.rv-zone-body { padding: 2px 18px 10px; }
/* The Highlighted / All fields toggle reuses the joined segmented control,
   at the far end of the Need-you head, with the count on each half. */
.rv-zone-head .filter-seg { margin-left: auto; }
.rv-toggle button { display: inline-flex; align-items: baseline; gap: 6px; }
.rv-toggle .n { font-size: 12px; font-weight: 600; color: var(--muted); }

/* A subject heading inside a zone: small quiet capitals, a trunk line
   running down from it into its fields, the group count a control of its
   own (hover to see the group, click to show it whole). Ported from the
   design canvas's tree, with the app's own tokens for the line colour. */
.rv-group-head {
    position: relative; display: flex; align-items: baseline; gap: 12px;
    padding: 12px 0 4px; font-size: 11px; font-weight: 600; letter-spacing: .08em;
    text-transform: uppercase; color: var(--muted);
}
.rv-group-head::after { content: ""; position: absolute; left: 6px; bottom: 0; height: 4px; border-left: 1.5px solid var(--tree-line); }
.rv-group-count {
    font: inherit; font-size: 12.5px; font-weight: 400; letter-spacing: 0; text-transform: none;
    color: var(--muted); background: none; border: 0; padding: 0; cursor: pointer;
}
.rv-group-count:hover, .rv-group-count[aria-expanded="true"] { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
/* The trunk runs the height of the group's rows; each field row is an elbow
   off it, and the last field ends the trunk (`.end`, no line under it). */
.rv-frow { position: relative; overflow: hidden; }
.rv-frow + .rv-frow { border-top: 1px dashed var(--border); }
.rv-frow::before { content: ""; position: absolute; left: 6px; top: 0; bottom: 0; border-left: 1.5px solid var(--tree-line); }
.rv-frow.end::before { display: none; }
.rv-group-head + .rv-frow .rv-fgrid { padding-top: 2px; }
.rv-flabel { position: relative; padding-left: 26px; font-weight: 500; }
.rv-flabel::before {
    content: ""; position: absolute; left: 6px; bottom: calc(50% - 1px);
    width: 12px; height: 200px; border-left: 1.5px solid var(--tree-line); border-bottom: 1.5px solid var(--tree-line);
    border-bottom-left-radius: 9px; box-sizing: border-box;
}
.rv-rules { margin-left: 6px; font-weight: 400; }
/* A field row: label, held value, arrow, proposed value, the kind label,
   then the actions. Six columns, so the arrow always points at the value
   beside it and the actions line up down the zone whatever the values do. */
.rv-fgrid {
    display: grid;
    grid-template-columns: 220px 190px 20px minmax(0, 1fr) 170px 280px;
    gap: 6px 12px; align-items: center; padding: 9px 0;
}
.rv-frow.quiet .rv-fgrid { padding: 6px 0; color: var(--text-2); }
.rv-frow.quiet .rv-flabel { font-weight: 400; color: var(--text-2); }
.rv-held { color: var(--muted); font-variant-numeric: tabular-nums; }
.rv-held.struck { text-decoration: line-through; }
.rv-held.none { font-style: italic; }
.rv-placeholder { display: block; margin-top: 2px; font-size: 12px; text-decoration: none; }
.rv-held.struck .rv-placeholder { text-decoration: none; }
.rv-group-count.still { cursor: default; }
.rv-group-count.still:hover { color: var(--muted); text-decoration: none; }
.rv-key .filter-seg { margin-left: auto; }
.rv-zone.decided { border-top: 1px solid var(--border); }
.rv-zone.decided .rv-frow + .rv-frow { border-top-style: dashed; }
.rv-held .rv-fw { margin-left: 4px; }
.rv-arrow { color: var(--muted); text-align: center; }
.rv-value {
    display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
    font-variant-numeric: tabular-nums;
}
.rv-new { font-weight: 600; font-size: 15px; border-bottom: 1px dotted var(--accent); cursor: help; }
.rv-frow[data-kind="overrule"] .rv-new:not(.plain) { color: var(--warn); }
/* A decided value is read, not judged, so it steps down a weight. */
.rv-new.plain { font-weight: 500; font-size: 14px; border-bottom-color: var(--muted); }
.rv-new.struck { text-decoration: line-through; color: var(--muted); }
.rv-note-under { flex-basis: 100%; font-size: 12.5px; color: var(--muted); }
/* The headshot in place of a value, unused since photos left this list, but
   kept for a field type that may want it again. */
.rv-photo { display: block; max-width: 120px; max-height: 150px; border: 1px solid var(--border); border-radius: 6px; cursor: help; }
.rv-qual, .rv-said { color: var(--muted); font-size: 13px; }
.rv-qual:empty { display: none; }
.rv-change {
    flex: none; padding: 1px 6px; border: 0; background: none; color: var(--muted); font-size: 12px; cursor: pointer;
}
.rv-change:hover { color: var(--text); }
/* The kind label: a small tag with the same dot or mark the closed row's
   chips carry, tinted by kind as the boards draw it, and only as wide as
   its words. */
.rv-kind {
    justify-self: start; display: inline-flex; align-items: center; gap: 5px;
    padding: 1px 8px 1px 6px; border-radius: 6px; border: 1px solid transparent;
    background: var(--chip); color: var(--text-2); font-size: 11.5px; font-weight: 600; white-space: nowrap;
}
.rv-kind .dot { width: 7px; height: 7px; margin: 0; }
.rv-kind.k-overrule { background: color-mix(in srgb, var(--warn-mark) 14%, transparent); color: var(--warn); }
.rv-kind.k-replace { background: color-mix(in srgb, var(--fw-mark) 8%, transparent); color: var(--fw-mark); }
.rv-kind.rv-held-link { background: var(--accent-tint); color: var(--accent-hover); cursor: help; }
.rv-acts { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: nowrap; text-align: right; }
.rv-decided { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; white-space: nowrap; }
.rv-decided.wrap { min-width: 0; white-space: normal; align-items: flex-start; text-align: left; }
.rv-decided.wrap svg { margin-top: 3px; }
.rv-decided.by-you { color: var(--text); font-weight: 500; }
.rv-decided.by-auto { font-weight: 500; }
.rv-decided .rv-when2 { margin-left: 2px; }
.rv-decided.by-auto { color: var(--ok); }
.rv-decided.by-off { color: var(--error); }
.rv-decided.pending { color: var(--text-2); cursor: help; }
.rv-decided svg { flex: none; }
.rv-when2 { font-weight: 400; color: var(--muted); }
.rv-wrong { flex: none; padding: 1px 7px; border: 0; background: none; color: var(--text-2); font-size: 13px; cursor: pointer; }
.rv-wrong:hover { text-decoration: underline; text-underline-offset: 2px; }
/* Approve and Reject as the boards draw them: white, outlined in the colour
   of what they do, with the tick or the cross in front. */
.rv-acts .button.rv-ok, .rv-acts .button.rv-rej { display: inline-flex; align-items: center; gap: 6px; background: var(--surface); font-weight: 500; padding: 5px 11px; border-radius: 8px; }
.rv-acts .button.rv-ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok-mark) 45%, transparent); }
.rv-acts .button.rv-rej { color: var(--error); border-color: color-mix(in srgb, var(--error-mark) 45%, transparent); }
.rv-acts .button.rv-ok:hover { background: color-mix(in srgb, var(--ok-mark) 8%, var(--surface)); }
.rv-acts .button.rv-rej:hover { background: color-mix(in srgb, var(--error-mark) 8%, var(--surface)); }
.rv-wrong:hover { color: var(--text); }
.rv-state { display: block; margin-top: 3px; font-size: 12px; color: var(--muted); }
.rv-state:empty { display: none; }
.rv-input {
    width: 120px; font: inherit; font-size: 14px; padding: 3px 8px;
    border: 1px solid var(--border); border-radius: 6px;
    background: var(--surface); color: var(--text); font-variant-numeric: tabular-nums;
}
.rv-input.edited { border-color: var(--warn); box-shadow: 0 0 0 1px var(--warn); }
/* The box a value is being changed in, as D4 draws it: the accent's edge
   and a soft ring, since it is where the person is working. */
.rv-value .rv-input { border-color: var(--accent); border-radius: 8px; padding: 5px 10px; box-shadow: 0 0 0 3px var(--accent-tint); }
.rv-picks { display: inline-flex; gap: 6px; flex-wrap: wrap; }
select.rv-input { width: auto; min-width: 5em; }
.rv-input.rv-text { width: min(24em, 100%); font-variant-numeric: normal; }

/* "Beside it in Vincere": the rest of a linked set, quiet, under the field
   that belongs to it. Indented past the label column, roughly where the held
   value starts, so it reads as a note on the row above rather than a new one
   lined up with the tree. */
.rv-ctx {
    display: flex; align-items: baseline; gap: 8px 18px; flex-wrap: wrap;
    margin: 0 0 6px 232px; padding: 7px 12px; border-radius: 8px;
    background: var(--bg); font-size: 13px; color: var(--text-2);
}
.rv-ctx b { font-weight: 600; color: var(--text); }
.rv-ctx .cap { font-size: 10.5px; }
.rv-companion { display: block; margin-top: 2px; color: var(--muted); font-size: 13px; }
.rv-companion b { color: var(--text); font-weight: 600; }

/* The feedback strip: quick picks and an optional note, under the row it
   belongs to rather than a browser prompt. Shared by Reject, a changed value
   and "Wrong?". */
.rv-strip-fb {
    margin: 0 0 8px 232px; padding: 10px 14px; border-radius: 10px;
    background: var(--bg);
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; color: var(--text-2);
}
.rv-pick {
    font: inherit; display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px;
    border: 1px solid var(--border-strong); background: var(--surface); color: var(--text); font-size: 12.5px; font-weight: 500; cursor: pointer;
}
.rv-pick[aria-pressed="true"] { background: var(--accent-tint); border-color: var(--accent); color: var(--accent-hover); font-weight: 600; }
.rv-strip-fb .rv-note {
    flex: 1 1 220px; min-width: 180px; font: inherit; font-size: 13px;
    padding: 5px 9px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text);
}
.rv-strip-fb .rv-strip-acts { flex: none; display: flex; align-items: center; gap: 8px; width: 100%; margin-top: 2px; }

/* All fields: one list, a tint per state as a stripe on the left, shorter
   rows for what nothing changed. The key line above names the four states;
   column headings sit under it, on the same terms as any other table. */
.rv-key { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; padding: 10px 16px; background: var(--bg); border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-2); }
.rv-key b { color: var(--text); font-weight: 600; }
.rv-swatch { display: inline-block; width: 13px; height: 13px; border-radius: 4px; margin-right: 6px; vertical-align: -2px; border: 1px solid var(--border-strong); }
.rv-swatch.need { background: var(--tint-need); border: 0; box-shadow: inset 3px 0 0 var(--accent); }
.rv-swatch.ok { background: var(--tint-ok); border: 0; box-shadow: inset 3px 0 0 var(--ok-mark); }
.rv-swatch.off { background: var(--tint-off); border: 0; box-shadow: inset 3px 0 0 var(--border-strong); }
.rv-colheads {
    display: grid; grid-template-columns: 220px 190px 20px minmax(0, 1fr) 170px 280px;
    gap: 6px 12px; padding: 7px 18px; border-bottom: 1px solid var(--border);
}
.rv-colheads span { font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.rv-all .rv-group-head { padding: 16px 16px 4px; }
.rv-all .rv-group-head::after { left: 21px; }
.rv-all .rv-frow { padding: 0 16px; }
.rv-all .rv-frow::before { left: 21px; }
.rv-all .rv-flabel { padding-left: 24px; }
.rv-all .rv-tinted.tint-need { background: var(--tint-need); box-shadow: inset 3px 0 0 var(--accent); }
.rv-all .rv-tinted.tint-ok { background: var(--tint-ok); box-shadow: inset 3px 0 0 var(--ok-mark); }
.rv-all .rv-tinted.tint-off { background: var(--tint-off); box-shadow: inset 3px 0 0 var(--border-strong); }
.rv-all .rv-tinted .rv-fgrid { padding: 10px 0; }
.rv-all .rv-tinted > .rv-frow { padding: 0 16px; }
.rv-all .rv-tinted > .rv-strip-fb, .rv-all .rv-tinted > .rv-ctx { background: var(--surface); margin-bottom: 10px; }
.rv-all .rv-ctx, .rv-all .rv-strip-fb { margin-left: 250px; }

/* The source text under the pointer: what the item is, then why the value,
   then the whole text with the sentence marked and scrolled to. The head and
   the quote are sticky, so a long CV scrolls under them rather than pushing
   them out of view. */
/* Only the text scrolls (`.rv-pop-text`, inside its own overflow); the head,
   the reason and the quote are ordinary flex children above it, so they never
   need to move to stay in view. For an inferred value the reason comes first,
   ahead of what the item is, because the reason is the thing to judge. */
.rv-pop { display: flex; flex-direction: column; padding: 0; max-height: min(70vh, 520px); }
.rv-pop-head { flex: none; margin: 0; padding: 10px 14px; background: var(--surface); border-bottom: 1px solid var(--border); color: var(--muted); font-size: 12px; }
.rv-pop-why { flex: none; margin: 0; padding: 10px 14px; background: color-mix(in srgb, var(--warn-mark) 10%, var(--surface)); border-bottom: 1px solid color-mix(in srgb, var(--warn-mark) 30%, transparent); color: var(--warn); }
.rv-pop-why + .rv-pop-head { border-bottom: 1px solid var(--border); }
.rv-pop-quote { flex: none; margin: 0; padding: 8px 14px; background: var(--surface); border-bottom: 1px solid var(--border); font-size: 13.5px; }
.rv-pop-text { flex: 1 1 auto; min-height: 0; overflow-y: auto; white-space: pre-wrap; word-break: break-word; padding: 10px 14px 12px; font-size: 13.5px; }
.rv-pop-miss { margin: 0 0 8px; color: var(--warn); }
/* A tint alone was easy to scroll past in a CV; the halo is what makes the
   sentence findable at a glance. */
mark.rv-mark {
    background: color-mix(in srgb, var(--accent) 30%, transparent); color: inherit; padding: 0 2px; border-radius: 3px;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 30%, transparent);
}
.button.compact { padding: 4px 10px; font-size: 13px; }

/* The photos are a second queue rather than a note about this one, so the way
   to them is a button at the end of the title's line, where every page says
   how it stands. It was a boxed alert, then a line of quiet prose over the
   filters, and both read as something to get past on the way to the queue. */
/* The segmented control a plain <select> held before the redesign: the four
   states are equally likely choices, not a form field, so they read as
   buttons in a quiet tray (review.js keeps the same four values, on the
   pressed button's aria-pressed rather than a select's value). */
/* A joined button group, not a tray: the tabs above it are the tray, and
   two of the same shape stacked would read as one control drawn twice. The
   pressed button takes the chip grey and the text colour.

   It was .rv-seg until 2026-09-23, when Extraction's up-next/read/failed
   filter and the Photos tab's own history filter became the second and third
   uses of it. The shape was never Review's; the name was. */
.filter-seg { display: inline-flex; }
.filter-seg button {
    font: inherit; font-size: 13px; font-weight: 500; padding: 6px 13px; margin-left: -1px;
    border: 1px solid var(--border-strong); background: var(--surface); color: var(--text-2); cursor: pointer;
}
.filter-seg button:first-child { margin-left: 0; border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.filter-seg button:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.filter-seg button[aria-pressed="true"] { background: var(--chip); color: var(--text); font-weight: 600; position: relative; z-index: 1; }
.filter-seg button:hover:not([aria-pressed="true"]) { color: var(--text); background: var(--bg); }
/* Quieter than the ordinary select it still is: a lighter edge and smaller
   text, so choosing a colleague reads as part of the filter row rather than
   as a form of its own. */
select.rv-owner-select { min-width: 0; font-size: 13px; padding: 6px 30px 6px 12px; border-color: var(--border); color: var(--text-2); }
/* The key under the list (D1): the same dot and mark shapes the chips use,
   named once rather than left for the reader to infer from a legend of
   colours alone. */
.rv-legend { display: flex; align-items: center; gap: 18px 22px; flex-wrap: wrap; margin: 10px 2px 0; font-size: 12.5px; color: var(--text-2); }
.rv-legend .cap { flex: none; }
.rv-legend-need { display: inline-flex; align-items: center; gap: 6px; }
.rv-legend .rv-chip, .rv-legend .rv-need-pill { pointer-events: none; }

/* ── Candidate field bench ─────────────────────────────────────────────────── */
/* Two name columns, because the CRM's name for a field and the API's name for
   it are both load-bearing and neither is guessable from the other. The pair
   is the content of the page, so they get the width and the inputs stay
   narrow. Fixed layout so the permanent and contract tables, which are two
   separate tables in two cards, still line up as one grid down the page. */
.fb-table { table-layout: fixed; width: 100%; }
.fb-table th:nth-child(1), .fb-table td:nth-child(1) { width: 30%; }
.fb-table th:nth-child(2), .fb-table td:nth-child(2) { width: 30%; }
.fb-table th:nth-child(3), .fb-table td:nth-child(3) { width: 16%; }
.fb-table th { text-align: left; }
.fb-table td { vertical-align: middle; }
.fb-ui { font-weight: 600; }
.fb-ui .muted { font-weight: 400; }
.fb-now { font-family: var(--mono); font-size: 13px; }
.fb-table input[type="text"],
.fb-table select { width: 100%; max-width: 15em; }
.fb-warn { color: var(--warn); font-weight: 600; }

/* The literal request body. It is the last thing read before a write that
   cannot be undone, so it is monospaced and does not wrap into ambiguity. */
.fb-json {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 12px;
    font-family: var(--mono);
    font-size: 12.5px;
    overflow-x: auto;
    margin: 8px 0 0;
}

/* The annual salary that rides along with a monthly one. It is not a separate
   decision, so it does not get a row of its own; it sits under the proposed
   value and stays quiet. */
.rv-companion { display: block; margin-top: 4px; color: var(--muted); font-size: 13px; }
.rv-companion b { color: var(--text); font-weight: 600; }

/* ── The field mapping table on /extraction ───────────────────────────────── */
/* One table per panel of the candidate screen, in the screen's own order, so
   it can be read against the CRM in another tab. The screen's name for a field
   leads because that is the one a recruiter can check; our own label for it
   used to sit alongside and was simply a second name for the same thing. */
/* Fixed layout so the two panels, which are two separate tables, line up as
   one grid down the page rather than each sizing to its own longest cell. */
table.ft { margin-bottom: 18px; table-layout: fixed; width: 100%; }
/* The two name columns hold a screen label and an API id, both short, and
   both a fixed size: sized as a percentage they grew with the page while the
   meaning, the one column with a sentence in it, wrapped to four lines beside
   them. 185px fits the longest id (salary_months_per_year) and the longest
   label. The third column is left unsized so it takes whatever remains.
   The widths sit on a colgroup rather than on the cells because a
   fixed-layout table takes them from its first row alone, and the first row
   here is the panel heading spanning all three: widths on the second row's
   cells were read by nobody, which is how the columns came to sit at an equal
   third each whatever this rule said. */
table.ft col.ft-name { width: 185px; }
table.ft td:nth-child(2) code { white-space: nowrap; }
/* A screen box the extractor declines, kept in its place so the gap is
   explained; dimmed so it does not read as a field the model can name. */
tr.ft-excluded td { opacity: .6; }
table.ft th.ft-block {
    font-size: 14px; padding-top: 12px;
    border-bottom: 1px solid var(--border); color: var(--text);
}
/* Left aligned, against the centring a spanning heading gets from table.calls:
   there the heading labels a group of columns and belongs over them, here it
   names the panel the rows below sit in, so it reads as a section title and
   lines up with the first column under it. Spelled out to the same depth as
   the rule it overrides, which outranks a plain table.ft th.ft-block. */
table.ft thead tr:first-child:not(:last-child) th.ft-block { text-align: left; }
.ft-ui { font-weight: 500; }
/* Currency, salary type and months per year are just boxes on the screen but
   not in the model's answer: they are written alongside a value and never
   proposed alone. Screen order mixes them in, so the row says so instead. */
.ft-qual {
    margin-left: 8px; padding: 1px 6px; border-radius: 999px;
    border: 1px solid var(--border); background: var(--bg);
    color: var(--muted); font-size: 11px; font-weight: 400; white-space: nowrap;
}

/* ─── Bundles ─────────────────────────────────────────────────────────────────
   An item in a candidate's bundle, on the entity page and inside an expanded
   run: a muted line naming the kind, date and origin, then the text. A kind
   tag says what the item is by an icon beside the word: the three kinds
   borrowed the semantic colours until 2026-09-22, which spent green and amber
   on something that is neither good nor a caution. The icons are masks, so
   they take the tag's own text colour; the black in them is only the mask's
   shape and never reaches the screen. */
.bundle-item { margin: 0 0 12px; }
.bundle-item > p { margin: 0 0 4px; }
.tag.item-comment::before, .tag.item-file::before, .tag.item-email::before {
    content: ""; flex: none; width: 12px; height: 12px; background: currentColor;
    -webkit-mask: var(--icon) center / contain no-repeat; mask: var(--icon) center / contain no-repeat;
}
.tag.item-comment { --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3 4.5A1.5 1.5 0 0 1 4.5 3h7A1.5 1.5 0 0 1 13 4.5v5a1.5 1.5 0 0 1-1.5 1.5H7l-3 2.5V11A1 1 0 0 1 3 10z' fill='none' stroke='black' stroke-width='1.4' stroke-linejoin='round'/></svg>"); }
.tag.item-file { --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M4.5 2.5h5l3 3v8h-8z M9.5 2.5v3h3' fill='none' stroke='black' stroke-width='1.4' stroke-linejoin='round'/></svg>"); }
.tag.item-email { --icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect x='2.5' y='4' width='11' height='8' rx='1.5' fill='none' stroke='black' stroke-width='1.4'/><path d='M3 5l5 4 5-4' fill='none' stroke='black' stroke-width='1.4' stroke-linejoin='round'/></svg>"); }
/* The Fieldwork log missing from a record entirely (.tag.error, above) is
   worse than "not yet", which is normal for a few seconds after an approval. */
table.calls tr.failed td { color: var(--error); }

/* A short number or id box beside its label, where a full-width input would
   read as a form field rather than a setting. */
input.narrow { width: 70px; }

/* ─── Photos ──────────────────────────────────────────────────────────────────
   /review/photos: the headshots waiting on Review as a grid of faces, because
   "is this a person?" is answered by looking, many at a time. A tile is the
   picture alone until the pointer rests on it (or a key moves focus into it),
   and then a band over its lower edge names the candidate and offers the two
   decisions. A replacement tile is the held photo and the new one side by
   side, with the dates under both always on show. The pictures are all the
   same shape (the crop is a portrait, a little taller than wide) so a row
   lines up and an odd one is the thing the eye catches. */
.ph-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
/* A group is a section of the page, so its heading takes a heading's voice
   rather than the 15px a bare h2 falls back to. */
.ph-head h2 { margin: 0; font-size: 18px; line-height: 1.2; letter-spacing: -.01em; }
.ph-head h2 .sub { margin-left: 10px; font-size: 13px; font-weight: 400; color: var(--muted); }
.ph-lock { margin: 6px 0 0; }
.ph-progress { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 12px 0 0; }

.ph-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin-top: 14px; }
.ph-grid.pairs { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
.ph-grid > p { grid-column: 1 / -1; margin: 0; }

/* A group is hundreds of faces, so its grid shows a row and a half and the
   fade over the cut is the button that opens the rest (Kevin, 2026-09-23).
   Where the cut falls is a height the script sets, because how many faces a
   row holds is the window's answer and no number of tiles is a row and a
   half. The cut pays for itself twice: the page is a screen rather than
   twenty, and a lazy picture under it is never in the viewport, so the held
   photos of the tiles nobody asked for are never fetched from Vincere. */
.ph-peek { position: relative; }
.ph-peek.peeked { overflow: hidden; }
.ph-more {
    display: block; width: 100%; margin-top: 10px; padding: 0;
    border: 0; background: none; font: inherit; font-size: 13px; font-weight: 500;
    color: var(--accent); text-align: center; cursor: pointer;
}
.ph-more:hover { text-decoration: underline; }
/* Over the cut half row, deep enough that the fade reads as "there is more
   below" rather than as a line, with the words on the solid end of it. */
.ph-peek.peeked .ph-more {
    position: absolute; left: 0; right: 0; bottom: 0; margin: 0; height: 104px;
    display: flex; align-items: flex-end; justify-content: center;
    background: linear-gradient(to bottom, transparent, var(--surface) 72%);
}

.ph-tile { position: relative; min-width: 0; }
.ph-frame { position: relative; border-radius: 6px; }
/* Square, as the headshot is cut since 2026-09-22: the tile shows exactly
   what goes to Vincere, which draws it as a circle as wide as the picture. */
.ph-img {
    display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
    border: 1px solid var(--border); border-radius: 6px; background: var(--bg);
}
.ph-missing { display: flex; align-items: center; justify-content: center; padding: 8px; text-align: center; font-size: 12px; color: var(--muted); }

/* A pair sits on a block of its own (Kevin, 2026-09-29, chosen on the design
   canvas). On white, the gap inside a pair was the gap between pairs, so a row
   of four pairs read as eight faces and nothing said which old photo went with
   which new one. The block does the grouping, so the arrow is a badge on the
   seam rather than a column of its own, and each caption is one short line
   with its sentence on hover. The two photos stay the same size: they are
   compared, and a smaller old one was tried and rejected. */
.ph-tile.pair { padding: 8px; border-radius: 10px; background: var(--photo-pair); }
.ph-tile.pair:hover, .ph-tile.pair:focus-within { outline: 1px solid var(--accent); outline-offset: -1px; }
.ph-pair { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
.ph-arrow {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
    display: flex; align-items: center; justify-content: center; width: 24px; height: 24px;
    border: 1px solid var(--border-strong); border-radius: 50%; background: var(--surface);
    box-shadow: 0 1px 3px var(--shadow); color: var(--text-2); font-size: 13px; line-height: 1;
}
.ph-dates { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin-top: 6px; }
.ph-date { font-size: 12px; line-height: 16px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ph-date.new { color: var(--text); }

.ph-over {
    position: absolute; left: 0; right: 0; bottom: 0;
    display: none; flex-direction: column; gap: 4px;
    padding: 6px; border-radius: 0 0 6px 6px;
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    font-size: 12px; line-height: 1.3;
}
/* A pair carries its dates under the pictures, and the dates are what that
   tile is read for, so its band drops from the top edge instead, with the
   name and the buttons on one line to cover as little of the faces as it can. */
.ph-tile.pair .ph-over { bottom: auto; top: 0; padding: 7px 8px; border-radius: 6px 6px 0 0; }
.ph-over-head { display: flex; align-items: center; gap: 8px; }
.ph-over-head .ph-name { flex: 1; min-width: 0; font-size: 13px; }
.ph-over-head .ph-acts .button { flex: none; padding: 3px 10px; }
.ph-tile:hover .ph-over, .ph-tile:focus-within .ph-over { display: flex; }
.ph-name { color: var(--text); font-weight: 600; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* On a decided tile the name is under the picture rather than in the hover
   band, so it takes the band's size rather than the page's 14px. */
.ph-tile > .ph-name { display: block; margin-top: 5px; font-size: 12.5px; }
.ph-name:hover { text-decoration: underline; }
.ph-src { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ph-acts { display: flex; gap: 4px; }
.ph-acts .button { flex: 1; padding: 3px 4px; font-size: 12px; }

/* What the page has done to a tile since it loaded, marked on the tile's
   edge so a grid of written photos reads as done without a word on each. */
.ph-tile.busy .ph-img { opacity: .5; }
.ph-tile.written .ph-img { outline: 2px solid var(--ok); outline-offset: -2px; }
.ph-tile.failed .ph-img { outline: 2px solid var(--error); outline-offset: -2px; }
.ph-badge {
    position: absolute; top: 4px; right: 4px; padding: 1px 6px; border-radius: 999px;
    background: var(--surface); border: 1px solid var(--border); font-size: 11px; color: var(--muted);
}
.ph-tile.written .ph-badge { color: var(--ok); border-color: var(--ok); }
.ph-tile.failed .ph-badge { color: var(--error); border-color: var(--error); }
.ph-tile.set_aside .ph-img { opacity: .5; }

/* ── Writes ────────────────────────────────────────────────────────────────── */
/* One row per write (/writes). The facts of fixed shape (when, who, the
   candidate, the field, the two tags) take the width they need and never
   wrap; what the CRM held and what was written share the rest, each in its
   own column with the arrow between, so a photo pair sits side by side and
   the arrows line up down the table (Kevin, 2026-09-22). The held column
   shrinks to its own widest line (a long address included) rather than
   claiming a set share of the table, and is right aligned, so the old value
   meets the arrow whether it is a long address or a short "empty": a fixed
   percentage held the same width for two rows of nothing but photos and
   short text as for a page of addresses, which is the gap the real ledger's
   first two rows showed up. A picture keeps its whole shape in a fixed
   square: what Vincere held may be a strip or a logo, and on this page it
   is the record of what was replaced, not a thumbnail. */
table.calls.writes td { vertical-align: middle; }
table.calls.writes .w-fit { width: 1%; white-space: nowrap; }
table.calls.writes .w-was { width: 1%; white-space: nowrap; text-align: right; }
table.calls.writes .w-was .w-value { justify-content: flex-end; }
table.calls.writes .w-was .w-photo { margin-left: auto; }
table.calls.writes .w-arrow { width: 1%; padding: 6px 8px; color: var(--muted); }
table.calls.writes td.w-val { overflow-wrap: anywhere; }
/* Every other column shrinks to its content, so the table's spare width has
   to be given to one of them by name or the browser shares it out and opens
   a gap in the middle of the row: on the mock it left 250px between the
   field and the value it replaced. The written value takes it, being the
   one column whose content can be long and the last thing the row is read
   for, so the slack falls after it instead of inside the pair. */
table.calls.writes td.w-val:not(.w-was) { width: 100%; }
/* A date is provenance, not prose, so it takes the mono face like an id
   rather than the row's own text. */
table.calls.writes td.w-when { color: var(--muted); font-family: var(--mono); font-size: 12.5px; }
/* A candidate's writes of one sitting are one tbody: a faint line between its
   own rows and the full one under the last, so the full lines are what
   separate one candidate from the next. Dashed at full strength was tried
   first and at reading distance looked the same as solid. */
table.calls.writes tbody.w-sitting > tr > td { border-bottom: 1px solid color-mix(in srgb, var(--border) 35%, transparent); }
table.calls.writes tbody.w-sitting > tr:last-child > td { border-bottom: 1px solid var(--border); }
table.calls.writes tr.open td { background: var(--bg); }
table.calls.writes tr.detail > td { background: var(--bg); padding: 0 12px 14px; }
.w-value { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
.w-value .tag { margin-left: 0; }
.w-held { color: var(--muted); }
.w-held.replaced { text-decoration: line-through; }
.w-new { font-weight: 600; }
.w-qual { color: var(--muted); font-size: 12px; }
.w-also { display: block; margin-top: 3px; font-size: 12px; color: var(--muted); }
.w-who, .w-auto, .w-hover, .w-edited, .w-photo[title] { cursor: help; }
.w-auto { font-style: italic; color: var(--muted); }
.w-photo {
    display: block; width: 96px; height: 96px; box-sizing: border-box; object-fit: contain;
    border: 1px solid var(--border); border-radius: 6px; background: var(--bg);
}
.w-photo.none {
    display: flex; align-items: center; justify-content: center; padding: 6px;
    border-style: dashed; background: none; color: var(--muted); font-size: 12px; text-align: center;
}
.kv.w-detail { margin-top: 12px; }
.kv.w-detail dd { max-width: 100ch; }

/* Success and failed are how the write stands, so they carry the dot every
   state does in this app, on top of the tag chip's own tint; the chip alone
   read as a coloured label rather than a state. */
table.calls.writes .w-result { display: inline-flex; align-items: center; gap: 6px; }
table.calls.writes .w-result::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; }
table.calls.writes .w-result.ok::before { background: var(--ok-mark); }
table.calls.writes .w-result.error::before { background: var(--error-mark); }
/* A write the retry job is still waiting on or has given up, under the
   result tag: the state as a word, the reason or the next try on hover, and
   for an admin the two buttons that decide what happens next. */
.w-retry { display: block; margin-top: 4px; font-size: 12px; color: var(--muted); cursor: help; }
.w-retry-acts { display: flex; gap: 6px; margin-top: 6px; }

/* ── Jobs ──────────────────────────────────────────────────────────────────── */
/* Everything that runs in the background (/jobs). What is running now is a
   card per job with its steps as a checklist: done steps ticked with their
   counts and times, the current one with a bar under it, the ones to come
   muted. The two tables below share one set of column widths, so the jobs on
   a timer and the one-off jobs line up down the page as if they were one
   table with a break in it. */
.job-live { padding: 14px 20px; }
.job-live-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.job-live-elapsed { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; }
.job-dot { flex: none; align-self: center; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: run-pulse 1.4s ease-in-out infinite; }
.job-dot.waiting { background: var(--muted); animation: none; }
@media (prefers-reduced-motion: reduce) { .job-dot { animation: none; } }

.job-steps { list-style: none; margin: 10px 0 0; padding: 0; font-size: 13px; }
.job-steps li {
    display: grid; grid-template-columns: 18px minmax(0, 1fr) 220px 70px;
    column-gap: 10px; align-items: baseline; padding: 4px 0;
}
.job-steps .step-count, .job-steps .step-time { font-variant-numeric: tabular-nums; }
.job-steps .step-time { text-align: right; color: var(--muted); }
.job-steps li.done .step-mark { color: var(--ok); }
.job-steps li.current .step-label { font-weight: 600; }
.job-steps li.current .step-mark::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.job-steps li.todo, .job-steps li.skipped { color: var(--muted); }
.job-steps li.skipped .step-label { text-decoration: line-through; text-decoration-color: var(--border); }

/* The bar sits under the current step's text, from the label column to the
   end. A <progress> rather than a styled div: an element's inline width is
   refused by the page's content security policy, and without a value the
   browser draws the "working, total unknown" animation itself. */
progress.job-bar {
    grid-column: 2 / -1; width: 100%; height: 6px; margin-top: 5px;
    appearance: none; border: 0; border-radius: 3px; overflow: hidden;
    background: var(--border); color: var(--accent);
}
progress.job-bar::-webkit-progress-bar { background: var(--border); border-radius: 3px; }
progress.job-bar::-webkit-progress-value { background: var(--accent); border-radius: 3px; }
progress.job-bar::-moz-progress-bar { background: var(--accent); border-radius: 3px; }

table.calls.jobs { table-layout: fixed; margin-top: 0; }
table.calls.jobs td, table.calls.jobs th { vertical-align: baseline; }
table.calls.jobs col.j-name { width: 250px; }
table.calls.jobs col.j-when { width: 160px; }
table.calls.jobs col.j-last { width: 220px; }
table.calls.jobs col.j-next { width: 160px; }
table.calls.jobs col.j-act { width: 110px; }
table.calls.jobs td.j-act { text-align: right; padding-right: 0; }
table.calls.jobs .job-name { font-weight: 600; font-size: 14px; }
table.calls.jobs .sub, table.calls.jobs td.sub-cell { color: var(--muted); }
table.calls.jobs .sub { font-size: 12px; margin-top: 1px; }
table.calls.jobs tr.open td { background: var(--bg); }
table.calls.jobs tr.detail > td { background: var(--bg); padding: 0 12px 12px; }
.job-state { color: var(--accent); }
.job-failed, .job-error { color: var(--error); }
.job-paused { color: var(--warn); }
.job-runs { max-height: 50vh; margin-top: 0; }

/* ── The switch: what runs without a person ───────────────────────────────── */
/* Added 2026-09-23 with the automatic/manual switch. Every stage of the
   pipeline says the same thing the same way: a face at the end of the title's
   line reading Automatic, Manual or Mixed, and behind it a panel of two-way
   switches, one per group.

   The dots carry the meaning and the words repeat it, never colour alone:
   green is the app's "runs by itself" and the accent its "needs a person",
   which is the same accent every main button and every open proposal wears.
   Mixed is both dots, overlapping, because a stage half automatic is not a
   third state of its own.

   The face is not a switch. It opens the panel and nothing else: a summary
   that could be clicked into another state would be read as a master switch
   over groups it does not know about. */
.sw-wrap { position: relative; display: inline-flex; }
.sw {
    display: inline-flex; align-items: center; gap: 8px;
    font: inherit; font-size: 13px; font-weight: 600; color: var(--text);
    padding: 6px 12px 6px 11px; border: 1px solid var(--border-strong);
    border-radius: 999px; background: var(--surface); cursor: pointer;
}
.sw:hover { border-color: var(--muted); }
/* Open, the face wears the accent ring so the panel below it reads as
   belonging to it rather than floating over the page. */
.sw[aria-expanded="true"] { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint); }
.sw .caret { color: var(--muted); font-size: 11px; margin-left: 2px; }
/* Two dots for Mixed, tucked into each other. */
.sw .dot + .dot { margin-left: -6px; }

/* The panel: a popover, anchored to the face and never taller than the
   window, since the review one lists every field the extractor owns. */
.sw-panel {
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 30;
    width: min(660px, calc(100vw - 56px)); max-height: min(70vh, 720px); overflow-y: auto;
    padding: 18px 22px 16px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: 0 12px 40px var(--shadow);
    text-align: left;
}
.sw-panel-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.sw-panel-head .cap { flex: none; }
.sw-panel-head h3 { margin: 0; font-size: 15px; font-weight: 600; }
/* What automatic means for this stage, said once at the top rather than on
   every row. */
.sw-panel-what { margin: 4px 0 8px; font-size: 13px; color: var(--text-2); }
.sw-panel .cap { margin-top: 16px; }
.sw-panel .cap:first-of-type { margin-top: 10px; }

/* One group: what it is, how it stands, and its switch at the end. */
.sw-row {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: center; gap: 8px 16px;
    padding: 11px 0; border-top: 1px solid var(--border);
}
.sw-row-name { font-weight: 600; }
.sw-row-note { display: block; font-size: 12.5px; color: var(--muted); }
/* Where a group's note is a few words (a fetcher's cadence, a record type's
   queue) the design puts it beside the name in a column of its own rather
   than under it, which halves the height of the panel. Review's two panels
   stack, a field's note being its counts. */
.sw-row.beside .sw-row-left { display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: 0 16px; align-items: baseline; }/* A group nothing can run for yet: a record type with no extractor, a
   fetcher that is not built, a field Vincere will not take. Greyed rather
   than hidden, so its absence is explained. */
.sw-row.unavailable { opacity: .55; }
.sw-row.unavailable .sw-seg { pointer-events: none; }
/* The settings a group's automatic mode needs, under its row and spanning
   it: the model a batch reads with, how many a wake takes, the daily cap. */
.sw-set {
    grid-column: 1 / -1; display: grid; grid-template-columns: 130px minmax(0, 1fr);
    align-items: center; gap: 10px 16px; font-size: 13px;
    margin: 2px 0 2px; padding: 14px 16px; background: var(--bg); border-radius: var(--radius-sm);
}
.sw-set > span:nth-child(odd) { color: var(--text-2); }
.sw-set input { width: 90px; }
.sw-set select { max-width: 260px; }
.sw-foot {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px;
    margin-top: 12px; padding-top: 14px; border-top: 1px solid var(--border);
    font-size: 13px; color: var(--text-2);
}
.sw-foot .end { margin-left: auto; }
.sw-note { font-size: 12.5px; color: var(--muted); }

/* The two-way control, one per group: a sunken tray with the chosen half
   standing off it, which is the tabs' own shape at control size. It is not
   the joined `.filter-seg` group, because this is a state being set rather
   than a view being chosen. */
.sw-seg {
    display: inline-flex; gap: 2px; padding: 2px; flex: none;
    background: var(--raised); border: 1px solid var(--border); border-radius: 9px;
}
.sw-seg button {
    display: inline-flex; align-items: center; gap: 7px;
    font: inherit; font-size: 12.5px; font-weight: 500; padding: 5px 11px;
    border: 1px solid transparent; border-radius: 7px; background: transparent;
    color: var(--text-2); cursor: pointer;
}
.sw-seg button[aria-pressed="true"] {
    background: var(--surface); border-color: var(--border);
    box-shadow: var(--shadow-card); color: var(--text); font-weight: 600;
}
.sw-seg button:disabled { cursor: default; }
/* The rules that hold whatever a switch says, shown as ticks because
   neither is a choice. The accent, not green: these two rows are the ones
   that always wait for a person, and green means runs-by-itself everywhere
   else on the app. */
.sw-tick {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; flex: none;
    border-radius: 5px; background: var(--accent); color: var(--btn-primary-fg); font-weight: 700; font-size: 12px;
}

/* ── A row that opens, and the list around it ─────────────────────────────── */
/* Kevin, 2026-09-23: "when you open a row it is not very visually distinct
   what the row content is to the rest of the page." Six shapes were drawn for
   it. Since 2026-09-24 it is a square 1px accent frame, edge to edge in the
   panel, with the head washed in the same colour, and the rest of the list
   fades to 60% while it is open.

   Nothing lifts and nothing moves. The open item keeps its place, its columns
   and its white, so the list never jumps as a row opens. The one accent, which
   everywhere else means "needs a person", is exactly right on the row a person
   is working on.

   What it replaced, on 2026-09-23 and for a day: a 1.5px frame inset 6px from
   the panel with rounded corners, and the rest faded to 42%. Kevin kept the
   frame and the wash, squared the frame off, and asked for the fade back a
   step lighter: at 42% the other rows were hard to read, and at 60% they
   still are readable while plainly not the row being worked on.

   Two classes, used by every list that opens a row: `.open-row` on the item,
   and `.opens` on the panel that holds them. `.open` alone was not free: the
   archive tables have used it for an expanded table row since long before
   this. */
/* Two things make it one frame rather than a frame beside a hairline. The
   -1px margin lays it over the lines already there, its own top hairline, the
   next row's and the panel's edge, and the z-index is what paints it above
   them: the row below would otherwise draw its grey over the blue, and a
   faded row is a layer of its own (opacity makes one), so `position:
   relative` alone is not enough while the fade is on. It is safe because
   neither popover lives inside a row; both are appended to the body. And the
   selector is two classes deep on purpose: `.rv-candidate + .rv-candidate`
   and `.src-source` both set a row's top border, and until 2026-09-24 they
   outranked the frame's, which is why its top edge drew grey. */
.opens > .open-row {
    position: relative; z-index: 1; margin: -1px;
    border: 1px solid var(--accent); border-radius: 0;
    background: var(--surface);
}
/* Square, except where the panel's own corner is round: an open row first or
   last in its panel follows that curve, or a square corner would stand out
   past the panel's edge. Review's first candidate is the case; a source never
   is, the captions being above it and the total below. */
.opens > .open-row:first-child { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); }
.opens > .open-row:last-child { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); }
/* The head takes the faintest wash of the frame's own colour; the body keeps
   the panel's white, as it has since 2026-09-04, when a darker tint made an
   open row read as a hole rather than as the piece being worked on. It is a
   step up from --accent-wash, which is cut to sit under body text and is
   almost invisible against a frame. */
.opens > .open-row > :first-child {
    background: color-mix(in srgb, var(--accent) 6%, transparent);
    border-top: 0;
}
.opens > .open-row:first-child > :first-child { border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0; }
/* While one is open the rest fade, so the eye goes to the only thing in full
   colour. :has rather than a class per row, so nothing has to be re-rendered
   to fade and the fade follows the open row wherever the list puts it. */
.opens:has(> .open-row) > :not(.open-row) { opacity: .6; }
.opens:has(> .open-row) > :not(.open-row):hover { background: none; }
/* Where a page draws more than one such panel (Review, one per prompt
   version), the panels that do not hold the open row step back whole. Without
   it the eye goes to the full-strength list under the faded one. */
.opens-page:has(.open-row) > .opens:not(:has(.open-row)) { opacity: .6; }

/* ── Sources: a row per source ────────────────────────────────────────────── */
/* Rewritten 2026-09-23. It was one table with a row per fact and a column per
   source, which read as a spreadsheet of the machine: browsing the archive,
   the thing a person actually comes here to do, was a link in the last row of
   it. Now each source is a row of its own with that button first among its
   actions, and the row opens into a panel with everything the columns could
   not hold. Kevin: the browse-the-archive button should be the main focus. */
.src-head, .src-row {
    display: grid;
    /* Every track is content-independent on purpose. The actions column was
       minmax(210px, auto), so it sized to its three buttons on a source's row
       and to nothing on the caption row above it, and every flexible column
       between them landed 47px apart: the captions did not sit over their own
       figures. */
    grid-template-columns: minmax(240px, 2fr) 110px minmax(160px, 1fr) 130px 80px minmax(150px, 1fr) minmax(210px, 336px);
    align-items: start; gap: 0 14px;
}
.src-head > *, .src-row > * { min-width: 0; padding: 15px 0; }
.src-head { padding: 0 18px; }
/* Column captions, in the small capitals every column head on the app wears:
   they are read once, on arrival, and the figures under them are the page. */
.src-head > * {
    padding: 11px 0; font-size: 11px; font-weight: 600;
    letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.src-row { padding: 0 18px; }
/* The hairline belongs to the block rather than to the row inside it, so an
   open source's frame replaces one line rather than drawing beside it. */
.src-source, .src-row.src-total { border-top: 1px solid var(--border); }
.src-num { text-align: right; font-variant-numeric: tabular-nums; }
/* The source's own name, in the voice a record's name takes in a list. */
.src-name { font-size: 16px; font-weight: 600; }
.src-what { display: block; margin-top: 3px; font-size: 12.5px; color: var(--muted); }
/* A figure with what qualifies it underneath: 4,612 held, and 2.9M characters
   of it. Smaller than a .stat-figure because there are five across a row
   rather than four across a page. */
.src-fig { font-size: 17px; font-weight: 600; letter-spacing: -.01em; line-height: 1.2; font-variant-numeric: tabular-nums; }
.src-fig .of { font-weight: 400; font-size: 13px; color: var(--muted); }
.src-sub { display: block; margin-top: 3px; font-size: 12px; color: var(--muted); }
/* How much of what is held a model is allowed to read. A bar rather than a
   second percentage, because the gap between the two figures is the whole
   point of the filters and 7% of 69M is hard to feel as a number. */
.src-bar { display: flex; height: 6px; width: 120px; margin-top: 7px; border-radius: 3px; overflow: hidden; background: var(--raised); }
.src-bar > i { background: var(--ok-mark); }
.src-acts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
.src-acts .button { padding: 7px 12px; }
/* The last row: the three sources added up, on the page's quiet fill so it
   reads as a total rather than as a fourth source. */
.src-row.src-total { background: var(--bg); border-radius: 0 0 var(--radius) var(--radius); }
.src-row.src-total > * { padding: 12px 0; font-size: 13px; color: var(--text-2); }
/* What is under an open source: how it is fetched, then what the archive is
   made of, in columns. The figures are not repeated, the row above still
   carrying every one of them in its own column. */
.src-open { border-top: 1px solid var(--border); padding: 14px 18px 18px; }
.src-about { margin: 0 0 14px; max-width: 100ch; font-size: 13px; color: var(--text-2); }
.src-detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 24px 32px; }
/* A list of counts that add up to the figure above them: held, then what each
   filter left. */
.src-dl { display: flex; flex-direction: column; font-size: 13px; }
.src-dl > div { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--border); }
.src-dl > div:last-child { border-bottom: 0; }
/* Wrapping rather than nowrap: nearly every value here is a figure, which
   never wraps anyway, and the one that is a sentence (a fetcher's last error)
   was running off the column's right edge. */
.src-dl .n { font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
.src-dl .t { color: var(--text-2); }
/* The last few wake-ups: the time in its own column, the sentence taking what
   is left. */
.src-runs { display: flex; flex-direction: column; font-size: 13px; }
.src-runs > div { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 10px; padding: 4px 0; border-bottom: 1px solid var(--border); }
.src-runs > div:last-child { border-bottom: 0; }
.src-runs .when { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.src-runs .line { color: var(--text-2); }

/* ── Extraction: the next run on one line ─────────────────────────────────── */
/* Rewritten 2026-09-23. The settings were a toolbar, the record up next was a
   two-column preview of that one record, and the queue, the ledger and the
   outcome counts were three sections further down. Kevin, on selecting five:
   "now it shows the next up in detail, but when you select 5 you still only
   see the first one up". So the settings are one line, what the model is sent
   is a fold under it, and one table shows either what is up next or what has
   been read, chosen by a joined filter. */
.run-line {
    display: flex; flex-wrap: wrap; align-items: center; gap: 12px 26px;
    padding: 14px 20px; border-bottom: 1px solid var(--border);
}
.run-set { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.run-set > .k { color: var(--text-2); }
.run-set b { font-weight: 600; }
.run-set select, .run-set input { padding: 5px 10px; font-size: 13px; }
.run-set input.count { width: 62px; text-align: center; }
/* There is no ceiling on how many records a run may read, so the box grows
   with the number typed into it (Kevin, 2026-09-23). Behind the @supports
   because width:auto on a number input is a wide box in a browser that cannot
   size a field to its content. */
@supports (field-sizing: content) {
    .run-set input.count { width: auto; min-width: 62px; field-sizing: content; }
}
.run-line .end { margin-left: auto; }
/* What the model is sent: shut until asked, and opening it is how a changed
   prompt gets read once rather than never. */
/* Dark, with a caret: this is a section heading a person opens, not the
   accent-coloured "show me the raw JSON" summary the generic rule draws. A
   flex summary loses the browser's own marker, so the caret is drawn here. */
.run-fold > summary {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px;
    padding: 12px 20px; cursor: pointer; font-weight: 600;
    color: var(--text); list-style: none;
}
.run-fold > summary::-webkit-details-marker { display: none; }
.run-fold > summary::before { content: "▸"; color: var(--accent); font-size: 11px; }
.run-fold[open] > summary::before { content: "▾"; }
.run-fold > summary:hover { color: var(--accent); }
.run-fold[open] > summary { border-bottom: 1px solid var(--border); }
/* The message takes what is left and the fields column is fixed, as the
   design has it: auto-fit split the two down the middle, and the message is
   the half worth reading. */
.run-open { display: grid; grid-template-columns: minmax(0, 1fr) 560px; gap: 24px 32px; padding: 16px 20px 20px; }
/* The message as it is sent, cut off rather than scrolled: the point of
   showing it is its voice, and the whole thing is a click away. */
.run-msg {
    position: relative; max-height: 190px; overflow: hidden;
    padding: 12px 14px; font-size: 13px; line-height: 1.5; color: var(--text);
    background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
    white-space: pre-wrap; word-break: break-word;
}
.run-msg::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 56px;
    /* Transparent to the panel's own fill, not to a literal copy of it:
       gradients interpolate premultiplied, so `transparent` fades without
       the grey it used to pull through the middle. */
    background: linear-gradient(transparent, var(--bg));
}
/* The fields a run may fill, by the panel of the CRM screen they sit on, so
   the list reads as the record rather than as an alphabet. */
.run-fields { display: flex; flex-direction: column; gap: 9px; }
.run-fields > div { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 10px; align-items: baseline; }
.run-fields .n { font-size: 12.5px; font-weight: 600; color: var(--text-2); }
.run-fields .c { display: flex; flex-wrap: wrap; gap: 5px; }
/* The filter over the one table, and the line at its foot saying what the
   rest of it holds. */
.run-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 12px 20px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.run-filter .end { margin-left: auto; font-size: 13px; color: var(--muted); }
.run-foot {
    display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 12px 20px;
    border-top: 1px solid var(--border); background: var(--bg);
    font-size: 13px; color: var(--text-2); border-radius: 0 0 var(--radius) var(--radius);
}
.run-foot .end { margin-left: auto; color: var(--muted); }

/* ── Photos: a decision under each face ───────────────────────────────────── */
/* The Photos tab's history, added 2026-09-23 with the tab itself. Kevin liked
   the open/approved/rejected buttons on the candidate queue and asked why the
   photos did not have them. A decided photo is still a face, so it keeps its
   tile and gains a line under it saying what became of it and who said so.
   Grouped by the day it was decided, because that is how a person looks for
   one. */
.ph-day { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin: 22px 0 0; }
.ph-day:first-of-type { margin-top: 0; }
.ph-day .cap { flex: none; }
.ph-day .sub { font-size: 12.5px; color: var(--muted); }
.ph-dec { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-2); overflow: hidden; }
.ph-dec .who { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── The design page: the language drawn with itself ──────────────────────── */
/* /design shows every token and every shared part with the real classes, so
   these rules only lay the examples out: a label column beside each example,
   and the swatch grid design.js fills from this file's own :root block. A
   swatch is painted by the token it names (var() set by the script), which
   is why no colour is written here. */
.ds-rows { display: flex; flex-direction: column; }
.ds-row { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--border); }
.ds-row:first-child { border-top: 0; padding-top: 0; }
.ds-row:last-child { padding-bottom: 0; }
.ds-line { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; }
.ds-stack { display: flex; flex-direction: column; gap: 8px; }
.ds-stack .alert { margin: 0; }
.ds-flat { margin: 0; }
.ds-h1 { font-size: 36px; line-height: 1.1; font-weight: 600; letter-spacing: -.025em; }
.ds-mono { font-family: var(--mono); font-size: 12.5px; color: var(--muted); }
.ds-narrow { width: 72px; }
.ds-switch-rows { max-width: 560px; }
.ds-group { padding: 16px 0; border-top: 1px solid var(--border); }
.ds-group:first-child { border-top: 0; padding-top: 0; }
.ds-group:last-child { padding-bottom: 0; }
.ds-note { margin: 0 0 12px; max-width: 100ch; font-size: 13px; color: var(--text-2); }
.ds-tokens { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px 18px; }
.ds-token { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ds-swatch { flex: none; width: 36px; height: 36px; border-radius: var(--radius-sm); border: 1px solid var(--border); }
.ds-swatch.ds-none { background: var(--bg); border-style: dashed; }
.ds-token-text { display: flex; flex-direction: column; min-width: 0; }
.ds-token-text code { background: none; padding: 0; font-size: 12.5px; }
.ds-token-text .muted { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The rows of the list example: a hairline between them and the padding a
   row needs. The frame, the wash and the fade are .opens and .open-row. */
.ds-item { border-top: 1px solid var(--border); cursor: pointer; }
.ds-item:first-child { border-top: 0; }
.ds-item-head { display: grid; grid-template-columns: 220px 180px auto; align-items: center; gap: 16px; padding: 12px 22px; }
.ds-item-body { padding: 14px 22px 18px; font-size: 13.5px; color: var(--text-2); }
