/* ============================================================================
   Mendel component library
   ============================================================================

   Every visual pattern the app uses, defined once. Templates compose these
   classes and carry no `style=` attributes and no colour literals of their
   own; internal/web/templates_test.go enforces both.

   Read /styleguide in a running server to see every component in every tone
   and state. That page is generated from the same CSS, so it cannot drift.

   Sections:
       1. Base and layout        6. Callouts and empty states
       2. Navigation             7. Tables and key-value lists
       3. Page header            8. Forms and fields
       4. Cards and sections     9. Lifecycle ribbon
       5. Buttons and badges    10. Roadmap panel, logs, disclosure
*/

/* ==== 1. Base and layout =================================================== */

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

body {
    margin: 0;
    padding: 0;
    background: var(--surface-page);
    color: var(--ink-1);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-body);
}

.page {
    max-width: var(--page-max);
    margin: 0 auto;
    padding: var(--sp-5) var(--sp-5) var(--sp-8);
}

h1, h2, h3, h4 {
    color: var(--ink-1);
    line-height: var(--leading-tight);
    margin: 0;
}

a { color: var(--tone-progress); }
a:hover { color: var(--tone-progress-ink); }

code, .mono {
    font-family: var(--font-mono);
    font-size: 0.92em;
}
code {
    background: var(--surface-sunken);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
}

:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--radius-sm);
}

/* ---- Layout primitives --------------------------------------------------
   These exist so a template never writes `display:flex` inline. If a layout
   is not expressible with them, it belongs here as a named pattern rather
   than as a one-off in a template. */

/* Vertical rhythm. The single most common thing a page needs. */
.stack       > * + * { margin-top: var(--sp-4); }
.stack-tight > * + * { margin-top: var(--sp-2); }
.stack-loose > * + * { margin-top: var(--sp-5); }

/* Horizontal row, wrapping, vertically centred. */
.row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
}
.row-tight { gap: var(--sp-2); }
.row-top   { align-items: flex-start; }
/* Fields and the button that submits them, sitting on one line. The field's
   own bottom margin is for stacked forms and would push the button out of
   alignment here. */
.row-end   { align-items: flex-end; }
.row > .field { margin-bottom: 0; }
.row-baseline { align-items: baseline; }
/* Pushes this element and everything after it to the right. */
.row-spacer { margin-left: auto; }

/* The standard two-column detail page: substance left, context right. */
.split {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--sp-5);
    align-items: start;
}
@media (max-width: 880px) {
    .split { grid-template-columns: minmax(0, 1fr); }
}

.prose { max-width: var(--measure); }
/* A narrower measure, for text centred under a heading where a full line
   length would drift too far from the thing it explains. */
.prose-narrow { max-width: 48ch; margin-left: auto; margin-right: auto; }

.text-center { text-align: center; }
.pre-wrap { white-space: pre-wrap; }
/* A form that should not interrupt the line it sits on. `.actions form` covers
   the common case; this is for a form inline within running content. */
.inline { display: inline; }

/* ---- Text utilities ------------------------------------------------------ */
.muted     { color: var(--ink-2); }
.subtle    { color: var(--ink-3); }
.text-sm   { font-size: var(--text-sm); }
.text-xs   { font-size: var(--text-xs); }
.nowrap    { white-space: nowrap; }
.numeric   { font-variant-numeric: tabular-nums; }

/* An eyebrow: the small upper-case label above a value or section. */
.eyebrow {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--ink-3);
}

/* ==== 2. Navigation ======================================================== */

.nav {
    background: var(--brand);
    height: var(--nav-h);
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 0 var(--sp-5);
}
.nav-home {
    display: inline-flex;
    align-items: center;
    padding: var(--sp-1);
    margin-right: var(--sp-3);
    border-radius: var(--radius-sm);
}
.nav-home:hover { background: rgba(255, 255, 255, 0.12); }
.nav-logo {
    height: 26px;
    width: 26px;
    border-radius: var(--radius-sm);
    display: block;
}
.nav-link {
    color: rgba(255, 255, 255, 0.82);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: 500;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    white-space: nowrap;
}
.nav-link:hover {
    color: var(--ink-inverse);
    background: rgba(255, 255, 255, 0.10);
}
/* Where am I. The old nav had no active state at all, so every page looked
   like every other page in the one place that should always differ. */
.nav-link.is-active {
    color: var(--ink-inverse);
    background: rgba(255, 255, 255, 0.16);
    font-weight: 600;
}
.nav-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}
.nav-user {
    color: rgba(255, 255, 255, 0.6);
    font-size: var(--text-sm);
}
/* Count of things waiting on the reader. Uses the waiting tone, not red:
   an open decision is a request, not an error. */
.nav-badge {
    display: inline-block;
    background: var(--tone-waiting);
    color: #3d2900;
    font-size: var(--text-xs);
    font-weight: 700;
    padding: 0 6px;
    border-radius: var(--radius-pill);
    margin-left: var(--sp-1);
    min-width: 18px;
    text-align: center;
}

/* ==== 3. Page header ======================================================= */

.page-head { margin-bottom: var(--sp-5); }

.breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--text-sm);
    color: var(--ink-3);
    margin-bottom: var(--sp-2);
    flex-wrap: wrap;
}
.breadcrumb a { color: var(--ink-2); text-decoration: none; }
.breadcrumb a:hover { color: var(--tone-progress); text-decoration: underline; }
.breadcrumb-sep { color: var(--line-strong); }

.page-title {
    font-size: var(--text-lg);
    font-weight: 650;
    letter-spacing: -0.01em;
}
/* The kind of thing this page is about, set before the name. Replaces
   titles like "Variation: auth-via-oauth", where the prefix competed with
   the name for the reader's eye. */
.page-kind {
    display: block;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ink-3);
    margin-bottom: 2px;
}
.page-lede {
    color: var(--ink-2);
    max-width: var(--measure);
    margin: var(--sp-2) 0 0;
}

/* ==== 4. Cards and sections ================================================ */

.card {
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    padding: var(--sp-5);
}
.card + .card { margin-top: var(--sp-4); }
.card-flush { padding: 0; overflow: hidden; }
.card-tight { padding: var(--sp-3) var(--sp-5); }
/* Extra breathing room, for a card that is the only thing on the screen. */
.card-roomy { padding: var(--sp-7) var(--sp-5); }

.card-head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-3);
    margin-bottom: var(--sp-4);
}
.card-title {
    font-size: var(--text-md);
    font-weight: 650;
}
/* A link or button sitting in the card header, right-aligned. */
.card-head-action {
    margin-left: auto;
    font-size: var(--text-sm);
    white-space: nowrap;
}
/* A note under a heading.
   This used to carry a negative top margin, tuned to sit snugly under a
   .card-head's bottom margin. Anywhere else -- under a bare <h3>, which has no
   bottom margin of its own -- the negative pulled the note up over the heading
   and the two overlapped. A component whose spacing only works beneath one
   specific sibling is a component that will be wrong somewhere, so the note now
   owns ordinary margins and the head tightens itself instead. */
.card-note {
    color: var(--ink-2);
    font-size: var(--text-sm);
    margin: var(--sp-2) 0 var(--sp-4);
    max-width: var(--measure);
}
.card-head:has(+ .card-note) { margin-bottom: 0; }
.card-title + .card-note { margin-top: var(--sp-1); }

/* A rule between stacked blocks inside one card. */
.divider {
    border: 0;
    border-top: 1px solid var(--line);
    margin: var(--sp-4) 0;
}

/* ---- Metrics -------------------------------------------------------------
   A figure and what it is. Used for cost, budget, counts. */
.metrics {
    display: flex;
    gap: var(--sp-5);
    flex-wrap: wrap;
}
.metric-value {
    font-size: var(--text-xl);
    font-weight: 650;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--ink-1);
}
.metric-label {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
    margin-top: 2px;
}
.metric-alert .metric-value { color: var(--tone-failure); }

/* ---- Meter ---------------------------------------------------------------
   A bar with an optional marker. Money spent means little without the time
   it was spent over, so the marker shows how far through the period we are. */
.meter {
    position: relative;
    background: var(--surface-sunken);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    height: 10px;
    overflow: hidden;
}
.meter-fill {
    height: 100%;
    background: var(--tone-success);
    border-radius: var(--radius-pill);
}
.meter-over .meter-fill { background: var(--tone-failure); }
.meter-mark {
    position: absolute;
    top: -3px;
    width: 2px;
    height: 16px;
    background: var(--ink-1);
    border-radius: 1px;
}
.meter-wrap { position: relative; padding-top: 3px; }
/* A meter sharing a line with text rather than owning a block. */
.meter-inline { flex: 1 1 120px; min-width: 100px; display: block; }
.meter-inline .meter { display: block; }
.meter-inline .meter-fill { display: block; }

/* ==== 5. Buttons and badges =============================================== */

/* Buttons are chosen by ROLE, never by colour. There is exactly one primary
   per view: the thing the page wants you to do. Everything else is
   secondary, and destructive things are danger. Choosing a colour because
   it looked right on the day is how the variation page ended up with five
   different button colours inside one card. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.4;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.btn-primary {
    background: var(--brand);
    color: var(--brand-ink);
}
.btn-primary:hover { background: var(--brand-dark); color: var(--brand-ink); }

.btn-secondary {
    background: var(--surface-card);
    color: var(--ink-1);
    border-color: var(--line-strong);
}
.btn-secondary:hover { background: var(--surface-hover); color: var(--ink-1); }

.btn-danger {
    background: var(--surface-card);
    color: var(--tone-failure-ink);
    border-color: var(--tone-failure);
}
.btn-danger:hover { background: var(--tone-failure-bg); color: var(--tone-failure-ink); }

/* A button that is really a link: no chrome until hovered. */
.btn-ghost {
    background: transparent;
    color: var(--ink-2);
    padding-left: var(--sp-2);
    padding-right: var(--sp-2);
}
.btn-ghost:hover { background: var(--surface-hover); color: var(--ink-1); }

.btn-sm { padding: var(--sp-1) var(--sp-3); font-size: var(--text-xs); }
.btn-block { width: 100%; }

.btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
}

/* A row of buttons. Forms are inline-block so POST buttons sit in the row
   without each template re-inventing `display:inline`. */
.actions {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}
.actions form { display: contents; }
.actions-end { justify-content: flex-end; }

/* ---- Badges --------------------------------------------------------------
   Tone-driven, always. A badge whose colour is picked from a raw status
   string is the bug this whole system exists to prevent. */
.badge {
    display: inline-block;
    padding: 2px var(--sp-2);
    border-radius: var(--radius-pill);
    font-size: var(--text-xs);
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
    background: var(--tone-neutral-bg);
    color: var(--tone-neutral-ink);
}
.badge-neutral  { background: var(--tone-neutral-bg);  color: var(--tone-neutral-ink); }
.badge-progress { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }
.badge-waiting  { background: var(--tone-waiting-bg);  color: var(--tone-waiting-ink); }
.badge-success  { background: var(--tone-success-bg);  color: var(--tone-success-ink); }
.badge-warning  { background: var(--tone-warning-bg);  color: var(--tone-warning-ink); }
.badge-failure  { background: var(--tone-failure-bg);  color: var(--tone-failure-ink); }

/* A solid block of one tone's line colour, for documenting the palette. */
.swatch {
    display: inline-block;
    width: 44px;
    height: 18px;
    border-radius: var(--radius-sm);
    background: var(--tone-neutral);
    vertical-align: middle;
}
.swatch-neutral  { background: var(--tone-neutral); }
.swatch-progress { background: var(--tone-progress); }
.swatch-waiting  { background: var(--tone-waiting); }
.swatch-success  { background: var(--tone-success); }
.swatch-warning  { background: var(--tone-warning); }
.swatch-failure  { background: var(--tone-failure); }

/* An outlined badge for something that does not exist yet -- a proposed Hop
   among real ones. The dashed edge is the same "not reached" signal the
   lifecycle stepper uses for an upcoming stage. */
.badge-outline {
    background: transparent;
    border: 1px dashed currentColor;
}

/* Same shape, outlined: for a badge that must not compete with a filled one
   beside it, e.g. a count next to a status. */
.badge-quiet {
    background: transparent;
    color: var(--ink-2);
    border: 1px solid var(--line-strong);
}

/* ==== 6. Callouts and empty states ======================================== */

/* One component replaces the eleven hand-rolled amber boxes. Tone chooses
   the colour; the template chooses the tone from domain.Tone, never by eye. */
.callout {
    display: block;
    /* Spacing of its own, because a callout is not always inside a stack. It
       was not, on the deployment page, and the buttons beneath it sat flush
       against the error they were responding to. */
    margin-bottom: var(--sp-4);
    border: 1px solid var(--tone-neutral);
    border-left-width: 4px;
    border-radius: var(--radius-md);
    background: var(--tone-neutral-bg);
    color: var(--tone-neutral-ink);
    padding: var(--sp-3) var(--sp-4);
}
.callout-neutral  { border-color: var(--tone-neutral);  background: var(--tone-neutral-bg);  color: var(--tone-neutral-ink); }
.callout-progress { border-color: var(--tone-progress); background: var(--tone-progress-bg); color: var(--tone-progress-ink); }
.callout-waiting  { border-color: var(--tone-waiting);  background: var(--tone-waiting-bg);  color: var(--tone-waiting-ink); }
.callout-success  { border-color: var(--tone-success);  background: var(--tone-success-bg);  color: var(--tone-success-ink); }
.callout-warning  { border-color: var(--tone-warning);  background: var(--tone-warning-bg);  color: var(--tone-warning-ink); }
.callout-failure  { border-color: var(--tone-failure);  background: var(--tone-failure-bg);  color: var(--tone-failure-ink); }

/* A quieter callout, for list items. A list of five tone-filled blocks is a
   wall of colour that stops distinguishing anything; keeping the fill white and
   the tone on the edge lets a list stay scannable while each row still says
   what state it is in. */
.callout-quiet {
    background: var(--surface-card);
    color: var(--ink-1);
    border-color: var(--line);
    border-left-color: var(--tone-neutral);
}
.callout-quiet.callout-neutral  { border-left-color: var(--tone-neutral); }
.callout-quiet.callout-progress { border-left-color: var(--tone-progress); }
.callout-quiet.callout-waiting  { border-left-color: var(--tone-waiting); }
.callout-quiet.callout-success  { border-left-color: var(--tone-success); }
.callout-quiet.callout-warning  { border-left-color: var(--tone-warning); }
.callout-quiet.callout-failure  { border-left-color: var(--tone-failure); }
.callout-quiet a { color: var(--tone-progress); text-decoration: none; }
.callout-quiet a:hover { text-decoration: underline; }

/* A whole callout that is itself a link, used for list rows. Without the block
   display an anchor stays inline and its block children escape the box. */
a.callout {
    text-decoration: none;
    color: inherit;
}
a.callout:hover {
    background: var(--surface-hover);
    border-color: var(--line-strong);
}
/* Only a callout that IS a link gets link affordances on its title. These
   selectors were once spliced apart by an edit that inserted a rule between
   them, which left every callout title in the app underlined and blue --
   including "Settled", which links nowhere. */
a.callout .callout-title { text-decoration: underline; text-underline-offset: 2px; }
a.callout:hover .callout-title { color: var(--tone-progress); }

/* A stack manages the gaps between its own children, so a callout inside one
   must not add a second. */
.stack > .callout,
.stack-tight > .callout,
.stack-loose > .callout { margin-bottom: 0; }

.callout-title {
    font-weight: 650;
    font-size: var(--text-base);
    margin: 0 0 var(--sp-1);
}
.callout-body { margin: 0; font-size: var(--text-sm); }
.callout-body > * + * { margin-top: var(--sp-2); }
.callout .actions { margin-top: var(--sp-3); }
/* Links inside a callout inherit the tone ink; the default link blue would
   fight the fill it is sitting on. */
.callout a { color: inherit; text-decoration: underline; }

/* A pre-formatted error body, e.g. a deploy failure. */
.callout-detail {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    white-space: pre-wrap;
    word-break: break-word;
    margin: var(--sp-2) 0 0;
}

/* ---- Empty state ---------------------------------------------------------
   Says what would be here and how to make it appear. An empty region with
   no explanation reads as broken rather than as not-yet-started. */
.empty {
    color: var(--ink-3);
    font-size: var(--text-sm);
}
.empty-block {
    text-align: center;
    padding: var(--sp-6) var(--sp-4);
    border: 1px dashed var(--line-strong);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
}
.empty-block .empty-title {
    color: var(--ink-2);
    font-weight: 600;
    font-size: var(--text-base);
    margin-bottom: var(--sp-1);
}
.empty-block .actions { justify-content: center; margin-top: var(--sp-4); }

/* ==== 7. Tables and key-value lists ======================================= */

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
}
.table th {
    text-align: left;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
    padding: var(--sp-2) var(--sp-3);
    border-bottom: 1px solid var(--line-strong);
    white-space: nowrap;
}
.table td {
    padding: var(--sp-3);
    border-bottom: 1px solid var(--line);
    vertical-align: top;
}
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--surface-sunken); }
/* A right-aligned column needs a right-aligned header, or the label floats off
   over the neighbouring column and the reader has to count across to work out
   which figure is which. */
.table th.table-num,
.table td.table-num { text-align: right; }
.table td.table-num { font-variant-numeric: tabular-nums; }

/* Vertical rules, for a table wide enough that the eye loses the column on the
   way across. Not the default: on a narrow table they are clutter. */
.table-ruled th + th,
.table-ruled td + td { border-left: 1px solid var(--line); }
.table-ruled th, .table-ruled td { padding-left: var(--sp-4); padding-right: var(--sp-4); }
.table-fixed { table-layout: fixed; }
/* Rows that link somewhere: the name cell carries the link and the whole
   row highlights, so the click target is obvious. */
.table-link { font-weight: 600; text-decoration: none; }
.table-sub { color: var(--ink-3); font-size: var(--text-xs); display: block; margin-top: 2px; }

/* Wide tables scroll inside their card rather than the page. */
.table-scroll { overflow-x: auto; }

/* ---- Comparison table ----------------------------------------------------
   One column per variation, one row per thing being compared. The row header
   is sticky because the whole point is reading across, and a criterion name
   that scrolls away takes the meaning of the row with it. */
.compare-wrap { position: relative; }
.compare-scroll { overflow-x: auto; overflow-y: visible; }
.compare {
    border-collapse: collapse;
    min-width: 100%;
    font-size: var(--text-sm);
}
.compare th,
.compare td {
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
    text-align: center;
}
.compare thead th {
    background: var(--surface-sunken);
    font-weight: 650;
    border-bottom: 1px solid var(--line-strong);
}
.compare .row-header {
    font-weight: 650;
    text-align: left;
    background: var(--surface-sunken);
    min-width: 150px;
    position: sticky;
    left: 0;
    z-index: 1;
}
/* The row that actually decides something. */
.compare .select-row td { background: var(--tone-progress-bg); }
.compare .select-row .row-header { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }

.compare-more {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    background: linear-gradient(to right, transparent, var(--surface-card) 40%);
    padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-7);
    pointer-events: none;
    font-size: var(--text-xs);
    color: var(--ink-3);
}

/* The one column of running text in a table of short values. */
.compare .compare-prose {
    white-space: normal;
    text-align: left;
    min-width: 220px;
    max-width: 320px;
    color: var(--ink-2);
}

/* A row for something that already exists, in a table about what is proposed.
   It is context, not the decision. */
.table tr.is-existing td { background: var(--surface-sunken); color: var(--ink-2); }

/* ---- The channel grid --------------------------------------------------
   A whole cell is the click target, because a radio the size of a full stop in
   the middle of a wide column is a target you have to aim at. */
/* Every column but the platform name is centred -- headers included. They were
   right-aligned by borrowing the numeric column class, which put each label
   over the gap between its own column and the next. */
.matrix th:not(:first-child),
.matrix td:not(:first-child) { text-align: center; }
.matrix td { padding: 0; }
.matrix td:first-child { padding: var(--sp-3) var(--sp-4); }
.matrix-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    cursor: pointer;
    min-height: 44px;
}
.matrix-cell:hover { background: var(--surface-hover); }
.matrix-cell input { margin: 0; }
.matrix-current {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--tone-success-ink);
    white-space: nowrap;
}
/* The chosen pairing, so the grid says which one is live as well as which are
   possible. */
.matrix-cell:has(input:checked) {
    background: var(--tone-success-bg);
    box-shadow: inset 0 0 0 2px var(--tone-success);
}

/* Supported / not, in a grid where most cells are one or the other. The
   unsupported mark is deliberately faint: it is the absence of a fact, and a
   grid of red crosses reads as a wall of failures rather than a capability
   list. */
.cell-yes { color: var(--tone-success); font-weight: 700; }
.cell-no { color: var(--line-strong); }

.diffstat-add { color: var(--tone-success); font-weight: 600; }
.diffstat-del { color: var(--tone-failure); font-weight: 600; }

/* A cell whose value is still being fetched. */
.cell-spinner {
    width: 15px;
    height: 15px;
    border: 2px solid var(--line);
    border-top-color: var(--tone-progress);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    display: inline-block;
}

/* ---- Key-value list ------------------------------------------------------
   Replaces the "Details" tables on the Hop and Variation pages. A definition
   list, because that is what it is. */
.kv {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--sp-3) var(--sp-5);
    margin: 0;
    font-size: var(--text-sm);
}
.kv dt {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
    padding-top: 2px;
}
.kv dd { margin: 0; color: var(--ink-1); min-width: 0; }
@media (max-width: 600px) {
    .kv { grid-template-columns: minmax(0, 1fr); gap: var(--sp-1) 0; }
    .kv dd { margin-bottom: var(--sp-3); }
}

/* ---- Tabs ---------------------------------------------------------------- */
.tabs {
    display: flex;
    gap: var(--sp-1);
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--sp-5);
    overflow-x: auto;
}
.tab {
    padding: var(--sp-3) var(--sp-4);
    text-decoration: none;
    color: var(--ink-2);
    font-size: var(--text-sm);
    font-weight: 500;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
}
.tab:hover { color: var(--ink-1); background: var(--surface-hover); }
.tab.is-active {
    color: var(--tone-progress-ink);
    border-bottom-color: var(--tone-progress);
    font-weight: 650;
}
.tab-count {
    display: inline-block;
    background: var(--tone-neutral-bg);
    color: var(--ink-2);
    font-size: var(--text-xs);
    padding: 0 6px;
    border-radius: var(--radius-pill);
    margin-left: var(--sp-1);
}
.tab.is-active .tab-count { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }

/* ==== 8. Forms and fields ================================================= */

.field { margin-bottom: var(--sp-5); }
.field:last-child { margin-bottom: 0; }
.field > label {
    display: block;
    font-weight: 650;
    font-size: var(--text-sm);
    margin-bottom: var(--sp-1);
}
.field .hint {
    display: block;
    color: var(--ink-3);
    font-size: var(--text-sm);
    margin-bottom: var(--sp-2);
    max-width: var(--measure);
}
.input,
.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="date"],
.field input[type="number"],
.field textarea,
.field select {
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    font-size: var(--text-base);
    font-family: inherit;
    background: var(--surface-card);
    color: var(--ink-1);
}
.input:focus,
.field input:focus,
.field textarea:focus { border-color: var(--tone-progress); }
.field textarea { min-height: 120px; resize: vertical; line-height: var(--leading-body); }
.field textarea.textarea-sm { min-height: 90px; }
.field-tight { margin-bottom: var(--sp-3); }
.input-mono { font-family: var(--font-mono); }
.input-inline { width: auto; min-width: 260px; }

/* A Key Result's target: a comparison, a number, and what the number counts.
   Three inputs on one line, sized to what each holds -- the comparison is a
   fixed list, the number is short, and the unit takes whatever room is left. */
.target-fields {
    display: flex;
    flex-wrap: wrap; /* On a phone the unit drops to its own line rather than off the edge */
    gap: var(--sp-2);
    align-items: center;
}
.target-fields .target-comparator { flex: 0 0 auto; width: auto; }
.target-fields .target-value { flex: 0 0 7em; text-align: right; font-variant-numeric: tabular-nums; }
.target-fields .target-unit { flex: 1 1 8em; min-width: 0; }
/* A boolean Key Result has no number and no unit. Hiding them in CSS means the
   form is right before any script runs, and the server discards the fields
   regardless, so the two cannot disagree. */
.target-fields:has(.target-comparator option[value="done"]:checked) .target-value,
.target-fields:has(.target-comparator option[value="done"]:checked) .target-unit {
    display: none;
}
.target-fields select,
.target-fields input {
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    font-family: inherit;
    font-size: var(--text-base);
    background: var(--surface-card);
    color: var(--ink-1);
}

.field-row { display: flex; gap: var(--sp-5); flex-wrap: wrap; }
.field-row > .field { flex: 1; min-width: 200px; }

/* One open question and the answers offered for it. The suggestions are
   .choice cards, so answering is recognising rather than composing. */
.question {
    border: none;
    border-left: 3px solid var(--line-strong);
    padding: 0 0 0 var(--sp-4);
    margin: 0 0 var(--sp-5);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}
.question:last-of-type { margin-bottom: var(--sp-4); }
.question-text {
    padding: 0;
    margin-bottom: var(--sp-1);
    font-weight: 650;
}

/* A radio or checkbox presented as a selectable card. */
.choice {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-card);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    cursor: pointer;
}
.choice:hover { border-color: var(--tone-progress); background: var(--surface-sunken); }
.choice:has(input:checked) {
    border-color: var(--tone-progress);
    box-shadow: inset 0 0 0 1px var(--tone-progress);
    background: var(--tone-progress-bg);
}
.choice-title { font-weight: 650; }
/* A proposal is a choice with more to read than a one-line label. */
.choice.proposal { align-items: flex-start; }
.proposal .choice-note { display: block; margin-top: var(--sp-1); }
.proposal-diff {
    display: block;
    margin-top: var(--sp-3);
    padding-top: var(--sp-2);
    border-top: 1px solid var(--line);
    font-size: var(--text-sm);
    color: var(--ink-2);
}
.proposal-diff .eyebrow { display: block; margin-bottom: 2px; }

/* A checkbox with a label beside it, in a column of them. */
.check {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    cursor: pointer;
    margin-bottom: var(--sp-2);
    font-size: var(--text-sm);
}
.fieldset {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
    padding: var(--sp-3) var(--sp-4);
    margin: 0 0 var(--sp-4);
}
.fieldset legend { padding: 0 var(--sp-1); }
.fieldset > :last-child { margin-bottom: 0; }

/* A scrolling conversation in a sidebar card. */
.msg-thread {
    max-height: 280px;
    overflow-y: auto;
    margin-bottom: var(--sp-4);
}
.msg-thread > * + * { margin-top: var(--sp-2); }
.choice-note { font-size: var(--text-sm); color: var(--ink-2); margin-top: 2px; }

/* ==== 9. Lifecycle ribbon ================================================= */

/* The ribbon answers the three questions every page opens with: where does
   this stand, whose move is it, and what happens next. Tone drives every
   colour; templates read domain.Ribbon and never branch on a raw status. */

.tone-neutral  { --tone: var(--tone-neutral);  --tone-bg: var(--tone-neutral-bg);  --tone-ink: var(--tone-neutral-ink); }
.tone-progress { --tone: var(--tone-progress); --tone-bg: var(--tone-progress-bg); --tone-ink: var(--tone-progress-ink); }
.tone-waiting  { --tone: var(--tone-waiting);  --tone-bg: var(--tone-waiting-bg);  --tone-ink: var(--tone-waiting-ink); }
.tone-success  { --tone: var(--tone-success);  --tone-bg: var(--tone-success-bg);  --tone-ink: var(--tone-success-ink); }
.tone-warning  { --tone: var(--tone-warning);  --tone-bg: var(--tone-warning-bg);  --tone-ink: var(--tone-warning-ink); }
.tone-failure  { --tone: var(--tone-failure);  --tone-bg: var(--tone-failure-bg);  --tone-ink: var(--tone-failure-ink); }

.ribbon {
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-left: 4px solid var(--tone, var(--tone-neutral));
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    padding: var(--sp-4) var(--sp-5);
    margin-bottom: var(--sp-4);
}
.ribbon-neutral  { --tone: var(--tone-neutral); }
.ribbon-progress { --tone: var(--tone-progress); }
.ribbon-waiting  { --tone: var(--tone-waiting); }
.ribbon-success  { --tone: var(--tone-success); }
.ribbon-warning  { --tone: var(--tone-warning); }
.ribbon-failure  { --tone: var(--tone-failure); }

.ribbon-head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
    margin-bottom: var(--sp-4);
}
.ribbon-headline {
    font-size: var(--text-md);
    font-weight: 650;
    color: var(--ink-1);
}
/* Whose move it is, or how it ended. The single most useful fact on the
   page for someone arriving cold, so it gets a filled badge. */
.ribbon-actor {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 2px var(--sp-2);
    border-radius: var(--radius-pill);
    background: var(--tone-neutral-bg);
    color: var(--tone-neutral-ink);
}
.ribbon-actor-you     { background: var(--tone-waiting-bg);  color: var(--tone-waiting-ink); }
.ribbon-actor-mendel  { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }
.ribbon-actor-done    { background: var(--tone-success-bg);  color: var(--tone-success-ink); }
/* A terminal failure must not wear the same badge as a terminal success. */
.ribbon-actor-failed  { background: var(--tone-failure-bg);  color: var(--tone-failure-ink); }
.ribbon-actor-closed  { background: var(--tone-neutral-bg);  color: var(--tone-neutral-ink); }

/* ---- Ribbon foot ---------------------------------------------------------
   What happens next, and the buttons that do it. The primary action of a
   page lives HERE and nowhere else: the old pages scattered it across a
   sidebar card, three button rows inside a details table, and a demo panel,
   so there was no one place to look for "what can I do". Contextual actions
   still belong beside the content they act on. */
.ribbon-foot {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    flex-wrap: wrap;
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--line);
}
.ribbon-next {
    margin: 0;
    color: var(--ink-2);
    font-size: var(--text-sm);
    max-width: var(--measure);
    flex: 1 1 320px;
}
.ribbon-foot .actions { margin-left: auto; }

/* ---- Tracks and stages ---------------------------------------------------
   A stepper, not a set of radio buttons. Three things carry the direction of
   travel: arrows rather than plain rules between stages, arrows that stay
   coloured only as far as progress has actually got, and marks inside the
   dots — a tick or a cross — that a radio group would never have. */
.track { margin-bottom: var(--sp-3); }
.track:last-of-type { margin-bottom: 0; }
.track-na { opacity: 0.45; }
.track-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
    margin-bottom: var(--sp-1);
}
/* The note explains why a track is dimmed, so it must stay readable even
   though the track around it is not. --ink-3 is the floor for text that
   carries meaning; the dimming comes from .track-na, not from the colour. */
.track-note { text-transform: none; letter-spacing: 0; color: var(--ink-3); }

.stages {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-1) 0;
}
.stage {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: var(--text-sm);
    color: var(--ink-3);
    position: relative;
}
/* Connector: a shaft with an arrowhead, drawn as one clipped block. */
.stage:not(:last-child)::after {
    content: "";
    width: 22px;
    height: 9px;
    margin: 0 9px;
    flex-shrink: 0;
    background: var(--line-strong);
    clip-path: polygon(0 40%, 62% 40%, 62% 0, 100% 50%, 62% 100%, 62% 60%, 0 60%);
}
/* Only the traversed part of the track takes colour, so the ribbon reads as
   a progress bar at a glance. */
.stage-done:not(:last-child)::after { background: var(--tone, var(--tone-success)); }

.stage-dot {
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 1.5px solid var(--line-strong);
    background: var(--surface-card);
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    line-height: 1;
    color: var(--ink-inverse);
}
.stage-done .stage-dot {
    background: var(--tone, var(--tone-success));
    border-color: var(--tone, var(--tone-success));
}
.stage-done .stage-dot::after { content: "\2713"; }
.stage-done { color: var(--ink-2); }
.stage-current .stage-dot {
    background: var(--tone, var(--tone-progress));
    border-color: var(--tone, var(--tone-progress));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--tone, var(--tone-progress)) 25%, transparent);
}
.stage-current { color: var(--ink-1); font-weight: 650; }
.stage-failed .stage-dot {
    background: var(--tone, var(--tone-failure));
    border-color: var(--tone, var(--tone-failure));
}
.stage-failed .stage-dot::after { content: "\2715"; }
.stage-failed { color: var(--tone-ink, var(--tone-failure-ink)); font-weight: 650; }
/* Not reached yet: smaller and hollow, so it is plainly an empty slot ahead
   rather than an option alongside the others. */
.stage-upcoming .stage-dot { width: 11px; height: 11px; border-style: dashed; }
.stage-skipped { opacity: 0.5; }
/* A two-word stage label must not break across lines: half of "Comparing
   results" sitting under the other half reads as two stages, not one. */
.stage-label { white-space: nowrap; }

.stage-note {
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--ink-3);
    background: var(--surface-sunken);
    border-radius: var(--radius-pill);
    padding: 1px 7px;
}

/* ==== 10. Roadmap panel, logs, disclosure ================================= */

/* ---- Roadmap panel -------------------------------------------------------
   The real roadmap, scrolled to the Hop this page is about. Collapsed it is
   a wide, short letterbox; the toggle grows it in place rather than sending
   the reader to another page. */
.roadmap-panel {
    margin-bottom: var(--sp-4);
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    overflow: hidden;
}
.roadmap-panel-head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border-bottom: 1px solid var(--line);
    background: var(--surface-sunken);
}
.roadmap-panel-hint { font-size: var(--text-xs); color: var(--ink-3); }
.roadmap-panel-toggle { margin-left: auto; }
.roadmap-panel-all { font-size: var(--text-sm); white-space: nowrap; text-decoration: none; }
.roadmap-panel-all:hover { text-decoration: underline; }
.roadmap-panel-canvas {
    position: relative;
    overflow: hidden;
    cursor: grab;
    height: 170px;
    transition: height 0.18s ease;
}
.roadmap-panel.is-expanded .roadmap-panel-canvas { height: 480px; }

/* The full roadmap page, where the graph is the subject rather than context. */
.roadmap-canvas {
    position: relative;
    overflow: hidden;
    cursor: grab;
    min-height: 420px;
}

/* ---- Decision messages --------------------------------------------------
   Who said what, on a Decision. The author is what distinguishes these, so
   that is what the treatment keys on: Mendel's own notes sit back, an agent's
   are marked as coming from a machine that was asked, and yours read as
   ordinary content. */
.msg {
    border: 1px solid var(--line);
    border-left: 3px solid var(--line-strong);
    border-radius: var(--radius-md);
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-card);
}
.msg p { margin: var(--sp-1) 0 0; }
.msg-system { background: var(--surface-sunken); border-left-color: var(--tone-neutral); }
.msg-agent  { border-left-color: var(--tone-progress); }
.msg-user   { border-left-color: var(--brand); }

/* ---- Log panel ----------------------------------------------------------- */
.log {
    background: var(--surface-sunken);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    overflow: hidden;
}
.log-scroll { max-height: 300px; overflow-y: auto; padding: var(--sp-3); }
.log-tall .log-scroll { max-height: 520px; }
.log-table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}
.log-time {
    white-space: nowrap;
    color: var(--ink-3);
    padding-right: var(--sp-3);
    vertical-align: top;
}
.log-level {
    padding-right: var(--sp-2);
    vertical-align: top;
    white-space: nowrap;
    font-weight: 700;
}
.log-level-milestone { color: var(--tone-success); }
.log-level-error     { color: var(--tone-failure); }
.log-level-info      { color: var(--tone-progress); }
.log-level-debug     { color: var(--ink-3); font-weight: 400; }
.log-msg { word-break: break-word; color: var(--ink-1); }
.log-foot {
    font-size: var(--text-xs);
    color: var(--ink-3);
    padding: var(--sp-2) var(--sp-3);
    border-top: 1px solid var(--line);
    background: var(--surface-card);
}

/* ---- Disclosure ----------------------------------------------------------
   The consistent home for detail a newcomer should not have to read: raw
   statuses, per-model cost breakdowns, deploy history, estimator notes. */
.disclosure {
    border-top: 1px solid var(--line);
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
}
.disclosure > summary {
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-2);
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}
.disclosure > summary::-webkit-details-marker { display: none; }
/* Drawn with borders rather than set as a glyph: the character forms are tiny
   and vary by font, and this is a control, so its size should be a decision. */
.disclosure > summary::before {
    content: "";
    width: 0;
    height: 0;
    flex-shrink: 0;
    border-left: 8px solid var(--ink-3);
    border-top: 5.5px solid transparent;
    border-bottom: 5.5px solid transparent;
    transition: transform 0.12s ease;
}
.disclosure > summary:hover::before { border-left-color: var(--ink-1); }
.disclosure[open] > summary::before { transform: rotate(90deg); }
.disclosure > summary:hover { color: var(--ink-1); }
.disclosure-body { margin-top: var(--sp-3); }

/* ---- Modal --------------------------------------------------------------- */
.modal {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(16, 22, 26, 0.5);
    justify-content: center;
    align-items: center;
    z-index: 1000;
    padding: var(--sp-4);
}
/* Opened by a link to the modal's id. Doing this in CSS rather than JavaScript
   means the open state is in the URL: it survives a reload, the back button
   closes it, and it cannot get stuck open because a script failed to load. */
.modal:target { display: flex; }
.modal.is-open { display: flex; }
.modal-box {
    background: var(--surface-card);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-5);
    max-width: 560px;
    width: 100%;
}
.modal-title { font-size: var(--text-md); font-weight: 650; margin-bottom: var(--sp-2); }

/* ---- Spinner ------------------------------------------------------------- */
.spinner {
    display: inline-block;
    width: 30px;
    height: 30px;
    border: 3px solid var(--line);
    border-top-color: var(--brand);
    border-radius: 50%;
    animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 3s; }
    .roadmap-panel-canvas { transition: none; }
}

/* ---- Banner -------------------------------------------------------------- */
/* Full-bleed strip under the nav, for project-wide conditions. */
.banner {
    background: var(--tone-waiting-bg);
    color: var(--tone-waiting-ink);
    border-bottom: 1px solid var(--tone-waiting);
    padding: var(--sp-3) var(--sp-5);
    font-size: var(--text-sm);
}
.banner a { color: inherit; font-weight: 650; }
.banner-inner {
    max-width: var(--page-max);
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}

/* ==== 11. Draft review ====================================================

   The first screens a new project sees, where Mendel shows what it made of the
   brief and asks for a decision before anything is planned. Deliberately
   plainer and wider-spaced than the rest of the app: the reader has not
   learned any of Mendel's vocabulary yet, so there is nothing here to scan
   for. */

/* The agent's own account of how it read the brief. Set apart from the draft
   itself, because it is commentary the user is checking, not content they are
   editing. */
.draft-notes {
    background: var(--surface-sunken);
    border-left: 3px solid var(--line-strong);
    padding: var(--sp-4) var(--sp-4);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    margin-bottom: var(--sp-4);
}
.draft-notes h3 {
    margin: 0 0 var(--sp-1);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
}
.draft-notes p, .draft-notes ul { margin: 0 0 var(--sp-3); }
.draft-notes ul { padding-left: var(--sp-5); }
.draft-notes li { margin-bottom: var(--sp-1); }
.draft-notes > :last-child { margin-bottom: 0; }

/* What the first drafting pass worked out about a project, shown above the
   objectives that were written against it. Each item carries its own verdict:
   answered by an objective, declined with a reason, or -- the third state --
   not judged at all, which the colour deliberately does not reassure about. */
.consideration-list { list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.consideration {
    border-left: 3px solid var(--line-strong);
    padding: 0 0 0 var(--sp-4);
    margin-bottom: var(--sp-4);
}
.consideration:last-child { margin-bottom: 0; }
.consideration-kind {
    display: inline-block;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
}
.consideration-statement { margin: var(--sp-1) 0 var(--sp-2); }
.consideration-coverage { font-size: var(--text-sm); margin: 0; }
.consideration-coverage.covered   { color: var(--tone-success-ink); }
.consideration-coverage.declined  { color: var(--ink-2); }
.consideration-coverage.unjudged  { color: var(--tone-waiting-ink); }
.consideration:has(.covered)  { border-left-color: var(--tone-success); }
.consideration:has(.unjudged) { border-left-color: var(--tone-waiting); }

.draft-objective {
    border-left: 4px solid var(--brand);
    padding: 2px 0 2px var(--sp-4);
    margin-bottom: var(--sp-6);
}
.draft-kr {
    background: var(--surface-sunken);
    border-radius: var(--radius-sm);
    padding: var(--sp-3) var(--sp-4);
    margin: var(--sp-3) 0;
}
.draft-kr .field { margin-bottom: var(--sp-3); }
/* The blank row at the foot of a list. Dashed rather than solid so it reads as
   somewhere to type rather than as another item already there. */
.draft-kr-new, .draft-objective-new {
    border: 1px dashed var(--line-strong);
    background: none;
}
.draft-kr-new { border-radius: var(--radius-sm); padding: var(--sp-3) var(--sp-4); margin: var(--sp-3) 0 0; }
.draft-objective-new {
    border-left-width: 1px;
    border-radius: var(--radius-sm);
    padding: var(--sp-3) var(--sp-4);
    margin-bottom: var(--sp-5);
}
.draft-kr .field:last-child { margin-bottom: 0; }

/* A line's title with its Remove button on the right. */
.draft-row-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.draft-row-title { font-weight: 650; }
/* A saved line marked for removal collapses to its heading and a note, and
   its button reads Undo. It stays on the page until the save, so the mark can
   be taken back. */
.draft-removed-note { display: none; flex: 1; font-size: var(--text-sm); color: var(--ink-2); }
.draft-row-head > [data-remove-row] { margin-left: auto; }
[data-removed] > :not(.draft-row-head) { display: none; }
[data-removed] > .draft-row-head .draft-removed-note { display: block; }
[data-removed] > .draft-row-head > .draft-row-title { text-decoration: line-through; color: var(--ink-3); }
[data-removed] > .draft-row-head > .eyebrow { display: none; } /* The note names the key result */
.draft-kr[data-removed] { padding-block: var(--sp-2); }
.draft-kr[data-removed] > .draft-row-head { margin-bottom: 0; }
.draft-objective-body > [data-add-kr] { margin-top: var(--sp-1); }

/* A score the tuner gave this line, shown before approval rather than after:
   the point is to catch a vague key result while it is still cheap to fix. */
.tune { font-size: var(--text-sm); color: var(--ink-2); margin-top: var(--sp-1); }
.tune-badge {
    display: inline-block;
    padding: 1px 7px;
    border-radius: var(--radius-pill);
    font-weight: 650;
    margin-right: var(--sp-1);
}
.tune-excellent  { background: var(--tone-success-bg);  color: var(--tone-success-ink); }
.tune-good       { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }
.tune-needs-work { background: var(--tone-waiting-bg);  color: var(--tone-waiting-ink); }
.tune-poor       { background: var(--tone-failure-bg);  color: var(--tone-failure-ink); }

/* --- Setup script ------------------------------------------------------- */
/* A block of commands meant to leave the page and land in a terminal, so it
   is styled as something to copy rather than something to read. */

.script-block {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    overflow: hidden;
    margin-top: var(--sp-4);
}

/* When an input sits on top, the two read as one control. */
.script-input + .script-head { border-radius: 0; }

.script-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--line);
}

.script-label {
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
}

.script-body {
    margin: 0;
    padding: var(--sp-4);
    background: var(--surface-sunken);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: 1.55;
    color: var(--ink-1);
    /* Wrapped rather than scrolled: a command running off the right edge is a
       command the reader cannot check before pasting. Each line is its own
       block, so the element itself does no whitespace handling. */
    white-space: normal;
}

.label-mono {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}

/* Copy button: an icon, because the block is something to take away rather
   than read, and the label doubles as the "Copied" acknowledgement. */
.btn-copy {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-3);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface-card);
    color: var(--ink-2);
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
}

.btn-copy:hover { background: var(--surface-hover); color: var(--ink-1); }
.btn-copy:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

.copy-icon { flex-shrink: 0; }
.btn-copy-label { line-height: 1; }

/* One line of a setup script. The line the user must edit is called out, since
   pasting it unchanged is the mistake the whole block is arranged to prevent. */
/* One line per block, so the markup carries no newline of its own -- a block
   element plus a literal newline renders every script double-spaced. */
.script-line {
    /* One value, because two rules depend on it and they must agree: the
       highlighted line replaces this padding to bleed its background to the
       block edges, and has to add the indent back. Written twice, they drifted,
       and the first character of the highlighted line rendered off the edge. */
    --script-indent: 2.5ch;

    display: block;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    /* A wrapped command continues indented, the way a terminal or a devtools
       console shows it, so the continuation is not mistaken for a new command. */
    padding-left: var(--script-indent);
    text-indent: calc(-1 * var(--script-indent));
}

/* A blank line in the script keeps its height; without this an empty block
   collapses and the grouping the author intended disappears. */
.script-line:empty::after {
    content: "\200b";
}

.script-line-comment { color: var(--ink-3); }

.script-line-edit {
    display: block;
    background: var(--tone-warning-bg);
    color: var(--tone-warning-ink);
    font-weight: 600;
    border-left: 3px solid var(--tone-warning);
    /* Bleeds into the block's own padding so the highlight reaches the edges.
       The padding carries the hanging indent as well as that offset: the
       negative text-indent from .script-line still applies, so replacing the
       padding with the offset alone drags the first character off the edge. */
    margin-left: calc(-1 * var(--sp-4));
    padding-left: calc(var(--sp-4) - 3px + var(--script-indent));
    margin-right: calc(-1 * var(--sp-4));
    padding-right: var(--sp-4);
}

.script-note {
    margin: 0;
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface-sunken);
    border-top: 1px solid var(--line);
    font-size: var(--text-xs);
    color: var(--ink-3);
}

/* ==== 12. OKR timeline ====================================================

   Added for the Strategy page's Objectives tab. See
   dev/claude_plans/15_key_result_measurement.md.
*/

.tl { --tl-label: 320px; }

/* The scroll is a fallback for a genuinely narrow window, not the normal
   state: a min-width above the card's inner width put a scrollbar under every
   timeline at ordinary sizes. */
.tl-scroll { overflow-x: auto; }
/* Room on the right for the things that sit at 100% and are centred on it: the
   final month tick and a due-date dot on the last day of the window. Padding
   the grid rather than the track keeps the percentage mapping intact, since the
   1fr column simply becomes that much narrower. */
.tl-grid { min-width: 560px; position: relative; padding-right: 16px; }
@media (max-width: 720px) { .tl { --tl-label: 180px; } }

/* Today, drawn once across every row rather than per row, so the eye reads a
   single line down the page. */
.tl-today {
    position: absolute;
    top: 26px;
    bottom: 0;
    width: 2px;
    background: var(--ink-1);
    z-index: 2;
    pointer-events: none;
}
.tl-today::after {
    content: "today";
    position: absolute;
    top: -20px;
    left: 50%;
    transform: translateX(-50%);
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--ink-1);
    background: var(--surface-card);
    padding: 0 var(--sp-1);
    white-space: nowrap;
}

.tl-axis {
    display: grid;
    grid-template-columns: var(--tl-label) 1fr;
    align-items: end;
    height: 26px;
    border-bottom: 1px solid var(--line-strong);
}
.tl-months { position: relative; height: 100%; }
.tl-month {
    position: absolute;
    bottom: 2px;
    font-size: var(--text-xs);
    color: var(--ink-3);
    transform: translateX(-50%);
    white-space: nowrap;
}
.tl-month::before {
    content: "";
    position: absolute;
    left: 50%;
    top: -6px;
    height: 5px;
    width: 1px;
    background: var(--line-strong);
}

/* ---- Objective: the container, and where Hops hang ---------------------
   Hops attach to the Objective rather than to individual Key Results. Some
   serve one Key Result and others the whole Objective, and only the Objective
   link is recorded -- so per-row pills would put every Objective-scoped Hop on
   every row beneath it.

   They sit in the label column, not over the timeline. A Hop has no date: it is
   sequenced by what it depends on, not by the calendar. Laid across the axis
   they read as positioned in time, which is a claim the data cannot make. */
.tl-objective {
    display: grid;
    grid-template-columns: var(--tl-label) 1fr;
    gap: 0 var(--sp-4);
    align-items: start;
    padding: var(--sp-4) 0 var(--sp-2);
    border-top: 1px solid var(--line);
}
.tl-objective-label > * + * { margin-top: var(--sp-2); }
.tl-objective:first-of-type { border-top: 0; }
.tl-objective-name { font-weight: 650; font-size: var(--text-base); }
.tl-hops { display: flex; flex-wrap: wrap; gap: var(--sp-1); align-items: center; }
.tl-hop {
    font-size: var(--text-xs);
    padding: 1px var(--sp-2);
    border-radius: var(--radius-pill);
    text-decoration: none;
    border: 1px solid transparent;
    white-space: nowrap;
}
.tl-hop-progress { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }
.tl-hop-success  { background: var(--tone-success-bg);  color: var(--tone-success-ink); }
.tl-hop-waiting  { background: var(--tone-waiting-bg);  color: var(--tone-waiting-ink); }
/* Not started, because something upstream has not finished. Dashed for the same
   reason an unreached stage in the lifecycle stepper is dashed. */
.tl-hop-blocked {
    background: transparent;
    color: var(--ink-3);
    border-style: dashed;
    border-color: var(--line-strong);
}

/* ---- Key Result row ---------------------------------------------------- */
.tl-row {
    display: grid;
    grid-template-columns: var(--tl-label) 1fr;
    gap: 0 var(--sp-4);
    align-items: center;
    padding: var(--sp-2) 0;
}
.tl-row + .tl-row { border-top: 1px dotted var(--line); }
.tl-kr { padding-left: var(--sp-4); }
.tl-kr-name { font-size: var(--text-sm); color: var(--ink-1); }
.tl-kr-meta { font-size: var(--text-xs); color: var(--ink-3); margin-top: 1px; }

.tl-track {
    position: relative;
    height: 22px;
    display: flex;
    align-items: center;
}
/* The span from when the Key Result started to the day it is due. */
.tl-bar {
    position: absolute;
    height: 10px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    border: 1px solid var(--line);
    overflow: hidden;
}
.tl-fill { height: 100%; border-radius: var(--radius-pill); }
.tl-fill-success { background: var(--tone-success); }
.tl-fill-waiting { background: var(--tone-waiting); }
.tl-fill-failure { background: var(--tone-failure); }

/* No measurement, so no fill can be drawn. Hatching says "unknown" where an
   empty bar would say "zero", and those are not the same claim. */
.tl-bar-unknown {
    background: repeating-linear-gradient(
        45deg, var(--surface-sunken), var(--surface-sunken) 4px,
        var(--surface-card) 4px, var(--surface-card) 8px);
}

/* The day it is due. */
.tl-deadline {
    position: absolute;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    border: 2px solid var(--surface-card);
    transform: translateX(-50%);
    z-index: 1;
}
.tl-deadline-success { background: var(--tone-success); }
.tl-deadline-waiting { background: var(--tone-waiting); }
.tl-deadline-failure { background: var(--tone-failure); }
.tl-deadline-neutral { background: var(--tone-neutral); }

.tl-reading {
    position: absolute;
    font-size: var(--text-xs);
    white-space: nowrap;
    color: var(--ink-2);
    transform: translateY(-14px);
}

/* ---- The two figures that answer "is the spending working" -------------
   Stacked, with their labels in one column and their meters in another, so the
   comparison is a matter of looking down rather than remembering a number from
   the line above. */
.attain { display: grid; grid-template-columns: max-content 1fr max-content; gap: var(--sp-2) var(--sp-4); align-items: center; }
.attain-label { font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); white-space: nowrap; }
.attain-figure { font-size: var(--text-sm); color: var(--ink-2); white-space: nowrap; }
.attain-figure strong { color: var(--ink-1); font-variant-numeric: tabular-nums; }
/* Met is solid; on-track is the same bar continued in a lighter tone, so the
   two readings are one shape rather than two numbers to reconcile. Both fills
   are laid over the same track rather than stacked, so "met" is always a
   segment of "on track" and cannot be read as a separate quantity. */
.attain .meter { position: relative; display: block; }
.attain .meter-fill { position: absolute; inset: 0 auto 0 0; display: block; }
.attain-ontrack { background: var(--tone-success-bg); }

.tl-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    font-size: var(--text-xs);
    color: var(--ink-2);
    margin-top: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--line);
}
.tl-legend span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.tl-swatch { width: 22px; height: 8px; border-radius: var(--radius-pill); }


/* Prerequisites: a real list, because the alternative was indentation inside a
   paragraph, and a proportional font renders that as ragged columns. */
.prereqs { margin-bottom: var(--sp-4); }

.prereqs-title {
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin: 0 0 var(--sp-2) 0;
}

.prereqs-list {
    margin: 0;
    padding-left: var(--sp-5);
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    font-size: var(--text-base);
    color: var(--ink-2);
}

/* The value the script cannot supply for itself, asked for above it. */
.script-input {
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-bottom: none;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}

.script-input label {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-1);
}

.script-input input { width: 100%; }

.script-input-note {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--ink-3);
}

/* Once filled, the line is no longer something to act on, so it stops shouting
   while staying identifiable as the line that was completed. */
.script-line-filled {
    background: var(--tone-success-bg);
    color: var(--tone-success-ink);
    border-left-color: var(--tone-success);
}

.btn-copy[disabled] { opacity: 0.5; cursor: not-allowed; }

/* A requirement Mendel knows cannot be satisfied as stated. Saying so is the
   designed answer where Mendel cannot supply what the provider needs; the
   alternative is an instruction the console refuses without explanation. */
.limitation {
    color: var(--tone-warning-ink);
    background: var(--tone-warning-bg);
    border-left: 3px solid var(--tone-warning);
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
}

/* A credential that buys a capability rather than gating a deploy. Listed apart
   from the required ones, because shown among them its absence reads as a fault
   rather than as a choice not yet made. */
.optional-credential {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-3) 0;
}

/* A field that takes one DNS label, with the rest of the name shown after it.
   A full-width box invites a full domain, which is the one thing that cannot go
   here: a wildcard record covers a single label. Showing the suffix makes the
   shape of the answer visible before it is typed rather than after it is
   rejected. */
.input-suffixed {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}

/* Three classes deliberately: the general field rule is
   `.field input[type="text"] { width: 100% }`, which outweighs two classes and
   would otherwise keep this box full width -- the exact shape that invites a
   whole domain where one label belongs. */
.field .input-suffixed .input-label {
    /* Wide enough for a real label, narrow enough not to read as "put a whole
       domain here". */
    width: 18ch;
    flex: 0 0 auto;
}

.input-suffix {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--ink-3);
    white-space: nowrap;
}

/* The ladder to a working name. Ordered, because the order is the point: a
   certificate that will not issue is usually two rungs back at a record that
   does not resolve. */
.ladder {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
}

.ladder-step {
    display: flex;
    gap: var(--sp-3);
    align-items: flex-start;
}

.ladder-mark {
    flex: 0 0 auto;
    width: 20px;
    text-align: center;
    color: var(--ink-3);
}

.ladder-body { display: flex; flex-direction: column; gap: 2px; }
.ladder-name { font-weight: 600; font-size: var(--text-base); }
.ladder-detail { font-size: var(--text-sm); color: var(--ink-2); }

.ladder-step[data-state="done"] .ladder-mark { color: var(--tone-success); }
.ladder-step[data-state="done"] .ladder-name { color: var(--ink-2); font-weight: 400; }

.ladder-step[data-state="yourmove"] .ladder-mark { color: var(--tone-waiting); }
.ladder-step[data-state="yourmove"] .ladder-name { color: var(--tone-waiting-ink); }

/* A step nothing has reached yet is dimmed rather than hidden: seeing what is
   coming is useful, being invited to start it is not. */
.ladder-step[data-state="blocked"] { opacity: 0.5; }

/* Mendel has not looked yet. Deliberately not the waiting or your-move colours:
   an unknown step must not read as a step with an answer, or the page sends
   someone to their DNS provider over a lookup that has not happened. */
.ladder-step[data-state="checking"] { opacity: 0.6; }
.ladder-step[data-state="checking"] .ladder-mark { color: var(--ink-3); }

/* Mendel looked and could not tell. Not dimmed like "checking", because this is
   not a step in progress and it is not going to resolve itself by being waited
   on -- it is a gap in what Mendel knows, and it should be legible as one. Not
   the waiting or your-move colours either: nothing here is anybody's move, and
   the step it sits on may well be finished. */
.ladder-step[data-state="unknown"] .ladder-mark { color: var(--ink-3); font-weight: 700; }
.ladder-step[data-state="unknown"] .ladder-name { color: var(--ink-2); font-weight: 400; }
.ladder-step[data-state="unknown"] .ladder-detail { font-style: italic; }

/* Channel setup, kept to hand rather than surfaced only when something breaks.
   Collapsed because it is reference, not a task. */
.setup-details { margin: var(--sp-3) 0; }
.setup-details > summary {
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-2);
    padding: var(--sp-2) 0;
}
.setup-details > summary:hover { color: var(--ink-1); }
.setup-body { padding-top: var(--sp-2); }

/* A value someone has to retype into another tool, with a button beside it.
   Selecting a long TXT-style value with a mouse is fiddly and easy to get
   wrong by a character, which is the kind of mistake that surfaces an hour
   later as a certificate that never issued. */
.copyable {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    max-width: 100%;
}

.copy-value {
    font-family: var(--font-mono);
    word-break: break-all;
}

.btn-copy-inline {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 2px;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--ink-3);
    cursor: pointer;
    /* Present but quiet until wanted: the value is what is being read, and a
       button per row competing with it makes the table harder to scan. */
    opacity: 0.45;
    transition: opacity 0.12s ease, color 0.12s ease;
}

.copyable:hover .btn-copy-inline,
.btn-copy-inline:focus-visible { opacity: 1; }
.btn-copy-inline:hover { color: var(--ink-1); }
.btn-copy-inline:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.btn-copy-inline.is-copied { opacity: 1; color: var(--tone-success); }

/* The full name beneath a provider-relative host. Present because some providers
   want it, quiet because most do not. */
.record-alt {
    display: block;
    margin-top: 2px;
    font-size: var(--text-xs);
}

/* A short explanation attached to a value that is correct but not what was
   wanted. Without it, a production URL showing an address reads as Mendel
   having lost the domain rather than as a deploy that fell back. */
.note-inline {
    display: block;
    margin-top: var(--sp-1);
    max-width: 60ch;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--ink-3);
}

/* A link to a deployment that is not answering yet (serving-link.js). Still
   visibly a link, so it reads as "not yet" rather than as broken. */
a.is-waiting {
    opacity: 0.55;
    cursor: progress;
}
.serving-note {
    margin-left: var(--sp-2);
    font-size: var(--text-xs);
    color: var(--ink-3);
}
