/* ============================================================================
   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; }

/* The browser hides [hidden] with display: none at the lowest precedence, so
   any component that sets its own display (.callout's block) showed through
   it: the Vision's length note sat on screen at 211 words of 350. */
[hidden] { display: none !important; }

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;
}

/* An underline means a link, and nothing else. Emphasis is <strong> or <em>,
   never an underline and never a colour of its own, so a reader never has to
   hover to find out whether something is clickable. The brand colour is not
   emphasis either: in the app the brand colour is never text, underlined or
   otherwise. */
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-canvas);
    border-bottom: 1px solid var(--line);
    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: var(--surface-hover); }
.nav-logo {
    height: 26px;
    width: 26px;
    display: block;
}
.nav-link {
    color: var(--ink-2);
    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-1);
    background: var(--surface-hover);
}
/* 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-1);
    background: var(--brand-tint);
    box-shadow: inset 0 -3px 0 var(--brand);
    font-weight: 600;
}
/* A Mendel admin's way in, when an Installation Cap is nearly reached. */
.nav-link.nav-link-warning {
    color: var(--tone-warning-ink);
    background: var(--tone-warning-bg);
}
/* A Mendel admin in a project they are not a member of (see
   internal/web/mendel_admin_view.go). Meant to be impossible to mistake for
   the ordinary nav at a glance: a hazard stripe along the top, the failure
   tone behind everything, and a solid label leading the row. Crimson rather
   than warning or waiting, because those already mean something about the
   project; this is about who is looking. */
.nav.nav-mendel-admin-view {
    background-color: var(--tone-failure-bg);
    background-image: repeating-linear-gradient(
        -45deg,
        var(--tone-failure) 0 10px,
        var(--ink-1) 10px 20px);
    background-size: 100% 6px;
    background-repeat: no-repeat;
    border-bottom: 2px solid var(--tone-failure);
}
.nav-admin-mode {
    display: inline-flex;
    align-items: baseline;
    gap: var(--sp-2);
    margin-right: var(--sp-3);
    padding: var(--sp-1) var(--sp-3);
    border-radius: var(--radius-sm);
    background: var(--tone-failure);
    color: var(--ink-inverse);
    font-size: var(--text-sm);
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}
.nav-admin-mode-sub {
    font-weight: 600;
    letter-spacing: 0.02em;
    opacity: 0.9;
}
.nav-mendel-admin-view .nav-link.is-active {
    background: var(--surface-card);
    box-shadow: inset 0 -3px 0 var(--tone-failure);
}
.banner.banner-mendel-admin-view {
    background: var(--tone-failure-bg);
    color: var(--tone-failure-ink);
    border-bottom-color: var(--tone-failure);
}
.nav-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
}
/* The project you are in, with the way out to the others in brackets beside
   it. Text rather than a link: the name is where you are, not somewhere to go. */
.nav-project {
    color: var(--ink-1);
    font-size: var(--text-sm);
    white-space: nowrap;
}
.nav-project strong {
    font-weight: 600;
    max-width: 24ch;
    overflow: hidden;
    text-overflow: ellipsis;
    display: inline-block;
    vertical-align: bottom;
}
.nav-project-switch { color: var(--ink-3); }
.nav-project-switch a { color: var(--ink-2); }
.nav-project-switch a:hover { color: var(--ink-1); }
.nav-user {
    color: var(--ink-3);
    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); }
/* The Danger Zone: an action that is allowed and very likely to break
   something for good, behind a typed confirmation (doc 47 §6.1). */
.card-danger { border-color: var(--tone-failure); }
.card-danger .card-title { color: var(--tone-failure-ink); }
/* A whole card that is itself a link. Without the block display an anchor
   stays inline, so its box breaks across every line of its block children and
   each line is underlined -- the same trap as a.callout below. Only the title
   takes a link's affordances, as a callout's does. */
a.card {
    display: block;
    color: inherit;
    text-decoration: none;
}
a.card:hover {
    background: var(--surface-hover);
    border-color: var(--line-strong);
}
a.card .card-title { text-decoration: underline; text-underline-offset: 2px; }
a.card:hover .card-title { color: var(--tone-progress); }

.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); }

/* ---- Stat groups ---------------------------------------------------------
   Figures that each lead somewhere: grouped by what they are about, one per
   row so a label never has to squeeze under a wide number, and each a link
   to where it is explained. A figure with nowhere to go is a .stat-static. */
.stat-groups {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--sp-4);
}
.stat-group-title {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
    margin: 0 0 var(--sp-2);
}
.stat {
    display: flex;
    align-items: baseline;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    margin: 0 calc(-1 * var(--sp-3));
    border-radius: var(--radius-md);
    text-decoration: none;
    color: inherit;
}
a.stat:hover { background: var(--surface-hover); }
.stat-value {
    font-size: var(--text-lg);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    color: var(--ink-1);
    min-width: 4.5ch;
}
.stat-label { color: var(--ink-1); font-size: var(--text-sm); font-weight: 500; }
a.stat .stat-label { color: var(--tone-progress-ink); }
.stat-sub { display: block; color: var(--ink-3); font-size: var(--text-xs); font-weight: 400; }

/* ---- Column chart --------------------------------------------------------
   One column per period, height set from data-meter-height by meter-script.
   For a small count over time, where the shape matters more than an axis. */
.columns {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 72px;
    border-bottom: 1px solid var(--line);
}
.column {
    flex: 1 1 0;
    min-height: 1px;
    background: var(--tone-progress);
    border-radius: 2px 2px 0 0;
}
.column-empty { background: var(--line); }
.columns-axis {
    display: flex;
    justify-content: space-between;
    color: var(--ink-3);
    font-size: var(--text-xs);
    margin-top: var(--sp-1);
}

/* ---- Distribution --------------------------------------------------------
   A label, a bar sized by share, and the count: how many of a whole sit in
   each bucket, read down the page. */
.distribution {
    display: grid;
    grid-template-columns: minmax(10rem, max-content) 1fr 3ch;
    gap: var(--sp-2) var(--sp-3);
    align-items: center;
}
.distribution .meter { display: block; }
.distribution .meter-fill { display: block; background: var(--tone-progress); }
.distribution-count { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }

/* ---- 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;
}
/* The strong shade rather than the plain brand fill, because white on the
   plain one is 4.0:1: enough for the marketing site's large Try it, not for a
   button's body-size label. */
.btn-primary {
    background: var(--brand-strong);
    color: var(--brand-on);
}
.btn-primary:hover { background: var(--brand-strong-hover); color: var(--brand-on); }

.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:not(.btn) { color: var(--tone-progress); text-decoration: none; }
.callout-quiet a:not(.btn):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. Not a link styled as a button, which keeps
   its own colours: tone ink on a brand fill is unreadable. */
.callout a:not(.btn) { 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; }
/* A row the person chose not to answer: still there, plainly set aside. */
.table tr.is-skipped td:not(:last-child) { color: var(--ink-3); opacity: 0.55; }

/* 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. The Variations'
   names and the Winner row are sticky for the same reason reading down.

   The box scrolls both ways, capped short of the window so the docked
   conversation does not cover its foot. It has to: a box that scrolls
   sideways is the scroll container for sticky in both directions, so headers
   could never stick to the window while the table could still scroll
   sideways. */
.compare-wrap { position: relative; }
.compare-scroll { overflow: auto; max-height: calc(100vh - 12rem); }
.compare {
    /* Separate, not collapsed: a collapsed border belongs to the table and
       stays put while a stuck cell moves over it, leaving a sliver of the
       rows beneath showing through. Every rule here draws one side only, so
       the two look the same. */
    border-collapse: separate;
    border-spacing: 0;
    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);
    position: sticky;
    top: 0;
    z-index: 2;
}
.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, stuck beneath the names. Its
   offset is the head row's height, which selection.js measures. */
.compare .select-row td {
    background: var(--tone-progress-bg);
    position: sticky;
    top: var(--compare-head-h, 2.8rem);
    z-index: 2;
}
.compare .select-row .row-header { background: var(--tone-progress-bg); color: var(--tone-progress-ink); }
/* Stuck both ways, so above everything else that sticks. */
.compare thead .row-header,
.compare .select-row .row-header { z-index: 3; }

.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);
}

/* What the experiment measured. Tabular figures so two Variations' rates line
   up digit under digit: the whole point of the row is a comparison by eye. */
.compare td.compare-num { font-variant-numeric: tabular-nums; }

/* 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);
}

/* The design space as row groups (doc 53 §5): an option's row header is
   lighter than a dimension's, and a mark Mendel read from the code rather
   than the proposal is drawn hollow and labelled. */
.compare .design-option .row-header { font-weight: 400; padding-left: var(--sp-6); white-space: normal; }
.compare .design-dimension .row-header,
.compare [data-compose-row] .row-header { white-space: normal; max-width: 280px; }
.design-mark { color: var(--tone-progress); font-size: var(--text-md, 1rem); }
.design-mark-read { color: transparent; -webkit-text-stroke: 1px var(--tone-progress); }
.design-read { display: block; font-size: var(--text-xs); color: var(--ink-3); }
/* The column a new Variation is composed in (doc 53 §6). */
.compare .compose-col {
    background: var(--tone-progress-bg);
    text-align: left;
    min-width: 220px;
    white-space: normal;
}
.compare thead .compose-col { background: var(--tone-progress-bg); }
.compare .compose-col input[type="text"],
.compare .compose-col textarea { width: 100%; margin: var(--sp-1) 0; }
.compare .design-option .compose-col { text-align: center; }

/* 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); }
/* Something on the tab waits on the reader: the nav badge's waiting tone,
   whether or not the tab is current, since being on the page does not do it. */
.tab-count.tab-attention,
.tab.is-active .tab-count.tab-attention { background: var(--tone-waiting); color: #3d2900; font-weight: 700; }

/* ==== 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 measured value typed against a Key Result's target: short, and set as a number. */
.target-value { max-width: 8em; text-align: right; font-variant-numeric: tabular-nums; }

.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 choice whose body is more than a label -- a heading and points -- takes
   the width beside its box, so a heading's right-hand badge sits at the edge. */
.choice > .choice-body { display: block; flex: 1; min-width: 0; }
/* 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; }
/* Every proposal starts checked, so a checked one keeps its border and loses
   the tint: a page of them was one block of blue. The unchecked ones recede
   instead, which is the choice being made. */
.choice.proposal:has(input:checked) { background: var(--surface-card); }
.choice.proposal:has(input:not(:checked)) { background: var(--surface-sunken); }
.choice.proposal:has(input:not(:checked)) > span { opacity: 0.6; }
.proposal-approach {
    margin-top: var(--sp-2);
    font-size: var(--text-sm);
    color: var(--ink-2);
}
.proposal-approach > summary { cursor: pointer; }
.proposal-approach .approach-points { margin-top: var(--sp-1); }
/* What a proposed Variation depends on, settled or stubbed (doc 51 §4), and
   what it takes from the Hop's design space (doc 53 §3). */
.proposal-dependencies,
.proposal-choices {
    display: block;
    margin-top: var(--sp-2);
    font-size: var(--text-sm);
    color: var(--ink-2);
}
.proposal-dependencies .eyebrow,
.proposal-choices .eyebrow { display: block; }
.proposal-dependencies .approach-points,
.proposal-choices .approach-points { margin-top: var(--sp-1); }
/* A Variation's approach as points, wherever it is shown. */
.approach-points { margin: var(--sp-2) 0 0; padding-left: 1.25em; }
.approach-points > li + li { margin-top: 2px; }
/* The experiment's details on a Variation proposal, shown once one is asked for. */
/* Inverted so it also holds when the choice is unavailable and has no input. */
.live-traffic-row:not(:has(input[name="live_experiment"]:checked)) .live-experiment-only { display: none; }
.compare .live-traffic-row th { white-space: normal; font-weight: 400; max-width: 60rem; }
.compare .live-traffic-row .live-experiment-only { margin-top: var(--sp-2); max-width: 60rem; }

/* A checkbox with a label beside it, in a column of them. */
/* Top-aligned, with the box nudged onto the first line's centre, so a label
   that wraps does not leave its box floating between the lines. */
.check {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    cursor: pointer;
    margin-bottom: var(--sp-2);
    font-size: var(--text-sm);
}
.check > input[type="checkbox"] { flex: none; margin: 0.2em 0 0; }
.check > label { cursor: inherit; }
/* A choice that exists but cannot be made yet; a .help-tip beside it says why. */
.check.is-disabled { cursor: default; color: var(--ink-3); }
/* Input Needed's notifications toggle, above the queue. .check's display
   would otherwise outrank the hidden attribute input-watch.js lifts. */
.input-notify[hidden] { display: none; }
.input-notify [data-input-notify-note] { display: block; font-size: var(--text-xs); }
/* The legend is floated so it sits inside the box as its heading rather than
   on the border, where it cut the line and crowded the box's corner. */
.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 { float: left; width: 100%; padding: 0; margin-bottom: var(--sp-2); }
.fieldset > legend + * { clear: both; }

/* A "?" that explains the thing beside it on hover or keyboard focus. */
.help-tip { position: relative; flex: none; margin-top: 0.1em; }
.help-tip-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.15rem;
    height: 1.15rem;
    padding: 0;
    border: 1px solid var(--ink-3);
    border-radius: 50%;
    background: transparent;
    color: var(--ink-3);
    font-size: var(--text-xs);
    font-weight: 700;
    line-height: 1;
    cursor: help;
}
.help-tip-mark:hover, .help-tip-mark:focus-visible { color: var(--ink-1); border-color: var(--ink-1); }
.help-tip-body {
    position: absolute;
    right: 0;
    top: calc(100% + var(--sp-1));
    z-index: 10;
    width: 16rem;
    max-width: 70vw;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface-card);
    box-shadow: var(--shadow-2);
    color: var(--ink-2);
    font-size: var(--text-sm);
    font-weight: 400;
    line-height: 1.45;
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.12s;
}
.help-tip:hover .help-tip-body,
.help-tip:focus-within .help-tip-body { visibility: visible; opacity: 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);
}
/* The ribbon pinned to the top of the window once it has scrolled away
   (ribbon-pin.js): one line, the headline, whose move it is and the primary
   action. Fixed rather than sticky, so the page below does not jump when it
   appears; the script lines it up with the ribbon it stands for. */
.ribbon-bar {
    position: fixed;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-top: 0;
    border-left: 4px solid var(--tone, var(--tone-neutral));
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-2) var(--sp-4);
}
.ribbon-bar[hidden] { display: none; }
.ribbon-bar .ribbon-headline { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ribbon-bar form { margin: 0; }
.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 {
    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);
}
/* Buttons in the first column, each as wide as the widest so their left and
   right edges line up; the note for each runs off to its right. */
.ribbon-actions {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    align-items: center;
    column-gap: var(--sp-4);
    row-gap: var(--sp-3);
    margin-top: var(--sp-3);
}
.ribbon-actions form { display: contents; }
.ribbon-action-note {
    color: var(--ink-3);
    font-size: var(--text-sm);
    max-width: var(--measure);
}
@media (max-width: 640px) {
    .ribbon-actions { grid-template-columns: minmax(0, 1fr); row-gap: var(--sp-2); }
    .ribbon-action-note:empty { display: none; }
    .ribbon-action-note { margin-bottom: var(--sp-2); }
}

/* ---- 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; }
/* A proposed Hop's placement that could not hold, and why (doc 37 §4). */
.placement-refused { color: var(--tone-warning-ink); }
/* 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; }
/* A stage with a screen of its own links to it. It keeps the stage's colour,
   so the track still reads as progress; the underline is what says "link". */
a.stage-label { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
a.stage-label:hover { color: var(--tone-progress); }

.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 {
    /* What an edge's casing is drawn in, to match what it sits on. */
    --roadmap-canvas-bg: var(--surface-card);
    position: relative;
    isolation: isolate;
    overflow: hidden;
    cursor: grab;
    /* One finger scrolls the page it sits in; a pinch is the graph's. */
    touch-action: pan-x pan-y;
    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;
}

/* ---- The roadmap page ----------------------------------------------------
   The graph is the page: it fills everything under the nav, the page itself
   never scrolls, and the title, the hints, the opened Hop and the request
   float over it. */
.body-full-bleed {
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.body-full-bleed > .nav,
.body-full-bleed > .banner { flex: 0 0 auto; }
.page-full-bleed {
    flex: 1 1 auto;
    min-height: 0;
    max-width: none;
    width: 100%;
    margin: 0;
    padding: 0;
    position: relative;
}
.roadmap-stage { position: absolute; inset: 0; overflow: hidden; }
.roadmap-stage-canvas {
    position: absolute;
    inset: 0;
    overflow: hidden;
    cursor: grab;
    /* The graph layers itself over the mainline's band (z-index 1 and 2);
       isolated, those stay inside the canvas, under the panels and dialogs
       floating over it. */
    isolation: isolate;
    /* A double-click zooms; it should not also select a Hop's name. */
    user-select: none;
    /* Panning and pinching are the graph's (roadmap-view.js), not the
       browser's. */
    touch-action: none;
}
.roadmap-stage-canvas > .empty { padding: var(--sp-5); }
.roadmap-overlay {
    position: absolute;
    top: var(--sp-3);
    background: color-mix(in srgb, var(--surface-card) 88%, transparent);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    padding: var(--sp-2) var(--sp-3);
    pointer-events: none;
}
.roadmap-overlay-title { left: var(--sp-3); max-width: 40%; }
.roadmap-overlay-title .page-title { font-size: var(--text-base); }
.roadmap-overlay-hints {
    right: var(--sp-3);
    margin: 0;
    list-style: none;
    font-size: var(--text-xs);
    color: var(--ink-3);
    line-height: 1.7;
}
.roadmap-hint-plus {
    display: inline-block;
    width: 14px;
    height: 14px;
    line-height: 14px;
    text-align: center;
    border-radius: 50%;
    background: var(--brand-strong);
    color: var(--brand-on);
    font-weight: 600;
}
/* The hints give way to the opened Hop, which sits where they were. */
.roadmap-stage:has(.roadmap-hop-panel:not([hidden])) .roadmap-overlay-hints { display: none; }
.roadmap-empty {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-width: 440px;
    text-align: center;
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-5);
}
.roadmap-empty .actions { justify-content: center; }

/* The opened Hop, docked to the right over the graph. */
.roadmap-hop-panel {
    position: absolute;
    top: var(--sp-3);
    right: var(--sp-3);
    bottom: var(--sp-3);
    width: min(380px, calc(100% - 2 * var(--sp-3)));
    overflow-y: auto;
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-4);
}
.roadmap-hop-panel[hidden] { display: none; }
.roadmap-hop-detail > * + * { margin-top: var(--sp-3); }
.roadmap-hop-detail .field { margin-bottom: var(--sp-2); }
.roadmap-hop-detail-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}
.roadmap-close {
    border: 0;
    background: none;
    font-size: 22px;
    line-height: 1;
    color: var(--ink-3);
    cursor: pointer;
    padding: 0 var(--sp-1);
}
.roadmap-close:hover { color: var(--ink-1); }
.roadmap-hop-facts {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px var(--sp-3);
    margin: 0;
}
.roadmap-hop-facts dt { color: var(--ink-3); }
.roadmap-hop-facts dd { margin: 0; }
.roadmap-hop-section > .eyebrow { display: block; margin-bottom: var(--sp-1); }
.roadmap-hop-list,
.roadmap-hop-krs,
.roadmap-hop-variations { list-style: none; margin: 0; padding: 0; }
.roadmap-hop-list > li + li,
.roadmap-hop-krs > li + li,
.roadmap-hop-variations > li + li { margin-top: var(--sp-2); }
.roadmap-kr-meter { height: 6px; margin-top: 3px; }
.roadmap-kr-fill-success { background: var(--tone-success); }
.roadmap-kr-fill-progress { background: var(--tone-progress); }
.roadmap-kr-fill-waiting,
.roadmap-kr-fill-warning { background: var(--tone-waiting); }
.roadmap-kr-fill-failure { background: var(--tone-failure); }
.roadmap-kr-fill-neutral { background: var(--ink-3); }
.roadmap-overdue { color: var(--tone-failure); }
.roadmap-hop-variation-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
    font-size: var(--text-sm);
}
.roadmap-hop-variation-head a { font-weight: 600; min-width: 0; }
.roadmap-hop-variation-head .badge { flex: 0 0 auto; white-space: nowrap; }
.roadmap-hop-approach {
    margin: 2px 0 0;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The request, a button at the foot of the graph that opens into the form. */
.roadmap-ask {
    position: absolute;
    left: 50%;
    bottom: var(--sp-4);
    transform: translateX(-50%);
    display: flex;
    flex-direction: column-reverse;
    align-items: center;
    gap: var(--sp-2);
    width: min(560px, calc(100% - 2 * var(--sp-3)));
    pointer-events: none;
}
.roadmap-ask > * { pointer-events: auto; }
/* With a Hop open, the request centres on what is left of the graph. */
.roadmap-stage:has(.roadmap-hop-panel:not([hidden])) .roadmap-ask {
    left: calc((100% - min(380px, 100% - 2 * var(--sp-3)) - var(--sp-3)) / 2);
    width: min(560px, calc(100% - min(380px, 100% - 2 * var(--sp-3)) - 3 * var(--sp-3)));
}
.roadmap-ask-toggle { box-shadow: var(--shadow-2); }
.roadmap-ask-panel {
    width: 100%;
    max-height: calc(100vh - var(--nav-h) - 120px);
    overflow-y: auto;
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-4);
}
.roadmap-ask-panel[hidden] { display: none; }
.roadmap-ask-panel > * + *,
.roadmap-ask-panel form > * + * { margin-top: var(--sp-3); }
.roadmap-ask-mode { display: flex; gap: var(--sp-2); align-items: flex-start; }
.roadmap-ask-mode input { margin-top: 3px; }

/* ---- The mainline bar ----------------------------------------------------
   Drawn over the roadmap rather than in it (roadmap-view.js), so it is the
   canvas's full height and its labels stay put while the graph pans and
   zooms. The root is a zero-width line at the bar's centre, and the bar a
   solid line drawn on it, the same width at every zoom, as its label is. Nothing in it takes pointer events but
   the build button, so dragging across it still pans. The band is under the
   graph's svg (z-index 1, set by the renderer), so edges cross over it; the
   labels and the button are above. */
.roadmap-mainline {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 0;
    pointer-events: none;
    --side-gap: 18px;
}
.roadmap-mainline-band {
    position: absolute;
    top: 0;
    bottom: 0;
    left: -2px;
    width: 4px;
    background: var(--brand);
    z-index: 0;
}
.roadmap-mainline-cap,
.roadmap-mainline-side,
.roadmap-mainline-build { z-index: 2; }
.roadmap-mainline-cap {
    position: absolute;
    top: var(--sp-2);
    left: 0;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 3px 10px;
    background: var(--surface-card);
    border: 1px solid var(--brand);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    white-space: nowrap;
    line-height: 1.25;
    pointer-events: auto;
}
.roadmap-mainline-cap code {
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--brand);
    background: none;
    padding: 0;
}
.roadmap-mainline-cap span { font-size: var(--text-xs); color: var(--ink-3); }
/* The two sides, the same distance from the bar's centre. */
.roadmap-mainline-side {
    position: absolute;
    top: 62px;
    font-size: var(--text-xs);
    color: var(--ink-2);
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    padding: 1px 8px;
    white-space: nowrap;
}
.roadmap-mainline-left { right: var(--side-gap); }
.roadmap-mainline-right { left: var(--side-gap); }
/* The embedded panel is too short to spare them: they covered Hops. */
.roadmap-panel-canvas .roadmap-mainline-side { display: none; }
/* Starting a change from main: the request, with Build it now ticked. Clear
   of the request button, which sits at the foot of the page. */
.roadmap-mainline-build {
    position: absolute;
    bottom: 76px;
    left: -15px;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    pointer-events: auto;
    font: inherit;
}
.roadmap-mainline-plus {
    width: 30px;
    height: 30px;
    flex: 0 0 30px;
    border-radius: 50%;
    background: var(--brand-strong);
    color: var(--brand-on);
    font-size: 20px;
    line-height: 30px;
    text-align: center;
    box-shadow: var(--shadow-2);
}
.roadmap-mainline-build-label {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--brand-strong);
    background: var(--surface-card);
    border: 1px solid var(--brand);
    border-radius: var(--radius-pill);
    padding: 2px 8px;
    white-space: nowrap;
}
.roadmap-mainline-build:hover .roadmap-mainline-plus { background: var(--brand); }
/* Reporting a problem (doc 57 §3): a second button beside the +, above it. */
.roadmap-mainline-problem { bottom: 116px; }
.roadmap-mainline-problem .roadmap-mainline-plus {
    background: var(--surface-card);
    color: var(--brand-strong);
    border: 1px solid var(--brand);
    font-size: 16px;
    font-weight: 700;
    line-height: 28px;
}
.roadmap-mainline-problem:hover .roadmap-mainline-plus { background: var(--surface-sunken); }

/* The Report a problem dialog's images and choices. */
.field-label,
.problem-choices legend {
    display: block;
    font-weight: 650;
    font-size: var(--text-sm);
    margin-bottom: var(--sp-1);
}
.problem-choices { border: 0; padding: 0; margin: 0; }
/* Not the field's own label: each choice is a line of ordinary text. */
.field.problem-choices > label { font-weight: 400; margin-bottom: 0; }
.problem-choices > label + label { margin-top: var(--sp-2); }
.problem-choices .is-disabled { color: var(--ink-3); }
.problem-images { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.problem-images:empty { display: none; }
.problem-images + .problem-images-pick { margin-top: var(--sp-2); }
.problem-image {
    position: relative;
    width: 72px;
    height: 72px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--surface-card);
}
.problem-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
.problem-image button {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 0;
    background: var(--surface-card);
    box-shadow: var(--shadow-1);
    cursor: pointer;
    line-height: 20px;
    padding: 0;
}
.field > label.problem-images-pick { display: inline-flex; margin-bottom: 0; }
.problem-images-pick input[type="file"] { display: none; }
[data-problem-panel].is-dropping { outline: 2px dashed var(--brand); outline-offset: -6px; }

/* The report's page: its images at full size. */
.problem-report-text { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 32rem; overflow: auto; }
.problem-report-image { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--radius-md); }
.roadmap-mainline-build:focus-visible { outline: none; }
.roadmap-mainline-build:focus-visible .roadmap-mainline-plus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 45%, transparent); }

/* ---- The roadmap review ---------------------------------------------------
   A proposal is drawn on the roadmap page's stage, and the decision sits in a
   tray at its foot: the figures and the two buttons always in view, the cost
   review and what approving changes folded under them, and the conversation
   below. roadmap-review.js keeps the graph clear of the tray. */
.review-tray {
    position: absolute;
    left: var(--sp-3);
    right: var(--sp-3);
    bottom: var(--sp-3);
    max-height: 70%;
    overflow-y: auto;
    background: var(--surface-card);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-3) var(--sp-4);
}
/* With a Hop open, the tray keeps to what is left of the graph. */
.roadmap-stage:has(.roadmap-hop-panel:not([hidden])) .review-tray {
    right: calc(min(380px, 100% - 2 * var(--sp-3)) + 2 * var(--sp-3));
}
.review-tray-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3) var(--sp-5);
}
.review-tray-title { min-width: 0; }
.review-tray-title .breadcrumb { font-size: var(--text-xs); margin: 0; }
.review-tray-title .page-title { font-size: var(--text-base); margin: 0; }
.review-tray-figures {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-1) var(--sp-4);
    margin: var(--sp-2) 0 0;
    color: var(--ink-2);
}
.review-tray-figures strong { color: var(--ink-1); }
.review-tray-alert, .review-tray-alert strong { color: var(--tone-failure); }
.review-tray-decision { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-1); max-width: 520px; }
.review-tray-decision .actions { margin: 0; }
.review-tray-decision .callout { margin: 0; }
.review-tray-note { margin: 0; text-align: right; }
.review-tray-more { margin-top: var(--sp-2); }
.review-tray-more > summary { cursor: pointer; font-size: var(--text-sm); color: var(--tone-progress); }
.review-tray-body { padding-top: var(--sp-3); }
.review-tray-body > * + * { margin-top: var(--sp-4); }
/* The conversation is the tray's last part rather than a dock of its own. */
.review-tray .chat-dock {
    position: static;
    margin: var(--sp-3) 0 0;
    padding: var(--sp-3) 0 0;
    border: 0;
    border-top: 1px solid var(--line);
    border-radius: 0;
    box-shadow: none;
}
.review-tray > .card { margin: var(--sp-3) 0 0; }
@media (max-width: 700px) {
    .review-tray-decision { align-items: stretch; }
    .review-tray-note { text-align: left; }
}

/* ---- Conversation dock --------------------------------------------------
   The conversation on an open Decision, held at the bottom of the window so
   it is in reach from anywhere on a long proposal. It floats, so it takes
   --shadow-2 and not a card's --shadow-1. */
.chat-dock {
    position: sticky;
    bottom: var(--sp-3);
    z-index: 10;
    margin-top: var(--sp-5);
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-card);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
}
.chat-dock-thread > summary, .chat-dock-head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    margin-bottom: var(--sp-2);
}
.chat-dock-thread > summary { cursor: pointer; }
.chat-dock-thread .msg-thread { max-height: 40vh; }
/* The thread folds away to give the page back, so the fold says what a click
   does: an open thread pushes the dock up over the content it is about. */
.chat-dock-thread > summary::after {
    content: "Show";
    margin-left: auto;
    font-size: var(--text-sm);
    color: var(--tone-progress);
}
.chat-dock-thread[open] > summary::after { content: "Hide"; }
.chat-dock-composer {
    display: flex;
    align-items: flex-end;
    gap: var(--sp-3);
}
.chat-dock-composer textarea {
    flex: 1;
    min-width: 0;
    resize: vertical;
    min-height: 3.2em;
    max-height: 40vh;
    font: inherit;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    background: var(--surface-card);
    color: var(--ink-1);
}
.chat-dock-composer textarea:focus { border-color: var(--tone-progress); }
.chat-dock-actions { display: flex; flex-direction: column; gap: var(--sp-2); }
.chat-dock-waiting { margin: 0; }
@media (max-width: 600px) {
    .chat-dock-composer { flex-direction: column; align-items: stretch; }
    .chat-dock-actions { flex-direction: row; }
}

/* ---- 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); }

/* ---- Variation thread ----------------------------------------------------
   What happened to a Variation, oldest first (doc 42 §3). Each turn is a
   decision message, keyed on who spoke; a run's log is a disclosure inside
   the turn that produced it. */
.thread { list-style: none; margin: 0; padding: 0; }
.thread > * + * { margin-top: var(--sp-2); }
.thread-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-1) var(--sp-2);
}
.thread-who {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
}
.thread-title { font-weight: 600; }
.thread-meta { margin-left: auto; font-size: var(--text-sm); color: var(--ink-3); }
/* Feedback can be a whole build log (retry with the diagnosed fix), so it
   keeps its line breaks and scrolls rather than taking over the page. */
.thread-text {
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 12em;
    overflow-y: auto;
}
.thread-detail { font-size: var(--text-sm); color: var(--ink-2); }
.thread-turn .disclosure { margin-top: var(--sp-3); padding-top: var(--sp-2); }
/* What may be done now, beneath the latest turn. A box that needs words
   opens in place, full width, below the buttons' row. */
.thread-next {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--sp-2);
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--line);
}
.thread-question {
    flex-basis: 100%;
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--tone-waiting);
    border-radius: var(--radius-md);
    background: var(--tone-waiting-bg);
}
.thread-question-ask { margin-top: 0; }
.thread-action { display: flex; align-items: center; gap: var(--sp-2); margin: 0; }
.thread-compose > summary { list-style: none; }
.thread-compose > summary::-webkit-details-marker { display: none; }
.thread-compose[open] { flex-basis: 100%; order: 1; }
.thread-compose-form {
    margin-top: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface-card);
}

/* ---- 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); }

/* The confirmation dialog (confirm-dialog.js): a native <dialog>, so focus,
   Escape and the inert page behind it are the browser's, drawn as a modal box. */
.confirm-dialog {
    border: none;
    padding: 0;
    background: transparent;
    max-width: min(480px, calc(100vw - 2 * var(--sp-4)));
    width: 100%;
}
.confirm-dialog::backdrop { background: rgba(16, 22, 26, 0.5); }
.confirm-dialog-box {
    background: var(--surface-card);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    padding: var(--sp-5);
}
.confirm-dialog-message { margin: 0 0 var(--sp-4); color: var(--ink-1); line-height: 1.5; }
.confirm-dialog-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); flex-wrap: wrap; }

/* ---- 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); }

/* An objective is a few words meant to be remembered, so it is set larger than
   the lines under it. */
.field .objective-input { font-size: var(--text-md); font-weight: 600; }

/* A key result is one sentence carrying its own definition, so it is the thing
   to read in this block: set at the weight of a line someone is meant to check
   word by word, over a box that grows rather than scrolling sideways. */
.field .kr-line {
    font-weight: 500;
    min-height: 0;
    resize: vertical;
    line-height: 1.45;
}
/* Caveats about taking the reading: one closed line, with whatever is written
   showing after the label until the row is opened to edit it. */
.kr-notes { margin-top: var(--sp-2); margin-bottom: 0; }
.kr-notes > summary {
    cursor: pointer;
    font-size: var(--text-sm);
    color: var(--ink-3);
    /* One line, whatever the notes say: the preview is a reminder of what is
       in there, and the box below it is where it is read in full. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.kr-notes-label {
    /* Its own span only so it clears the native marker, which the letter
       spacing below otherwise crowds. */
    margin-left: var(--sp-1);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.kr-notes-preview { margin-left: var(--sp-2); color: var(--ink-2); }
/* Open, the box holds the notes, so the preview would be the same words twice. */
.kr-notes[open] > summary > .kr-notes-preview { display: none; }
/* How a key result will be measured (doc 43 §4): one line, and the means to
   change it folded away beneath it. */
.kr-route { margin-top: var(--sp-1); }
.kr-route-change > summary { cursor: pointer; color: var(--ink-2); }
.kr-route-measure {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    gap: var(--sp-2);
    margin-top: var(--sp-2);
}
.field.kr-notes > textarea {
    margin-top: var(--sp-1);
    min-height: 0;
    font-size: var(--text-sm);
    color: var(--ink-2);
    resize: vertical;
}

/* Context folded under the draft: how the brief was read, and what success
   depends on. The summary is the card's title, with a disclosure triangle. */
.disclosure-card > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    margin: 0;
}
.disclosure-card > summary::-webkit-details-marker { display: none; }
.disclosure-card > summary::before {
    content: "";
    align-self: center;
    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-card[open] > summary::before { transform: rotate(90deg); }
.disclosure-card[open] > summary { margin-bottom: var(--sp-3); }
.disclosure-card .draft-notes { margin-bottom: 0; }
/* 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); }

/* The grade the tuner gave this line, and the note that earned it, shown before
   approval rather than after: the point is to catch a vague key result while it
   is still cheap to fix.

   The letter carries the colour and nothing else does. It was a tinted block
   with a coloured rail, which tied the grade to its note and then outweighed
   the key result the two of them were about -- nine of them down a page, each
   louder than the objective above it. A coloured letter is enough to tie them,
   and running green to red across A to D says which way is better without
   anything else having to.

   A grade Mendel does not give still renders, in ink rather than in a tone.
   Silently having no styling would be the worse failure. */
.grade {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    margin: var(--sp-2) 0 0;
    font-size: var(--text-sm);
    color: var(--ink-2);
}
.grade-mark {
    flex: none;
    width: 1.7em;
    line-height: 1.7em;
    text-align: center;
    border-radius: var(--radius-sm);
    font-weight: 700;
    background: var(--surface-sunken);
    color: var(--ink-3);
}
/* Two classes rather than one, so a tone always beats the unlettered default
   above whatever order these end up in. */
/* Mixed from the mid tone rather than taken from its -bg, which is sized for a
   full-width callout and too pale to read as a colour in a 1.7em square. B was
   the one that vanished: pale yellow on an off-white card is barely a tint. */
.grade-mark.grade-a { background: color-mix(in srgb, var(--tone-success) 26%, white); color: var(--tone-success-ink); }
.grade-mark.grade-b { background: color-mix(in srgb, var(--tone-waiting) 36%, white); color: var(--tone-waiting-ink); }
.grade-mark.grade-c { background: color-mix(in srgb, var(--tone-warning) 26%, white); color: var(--tone-warning-ink); }
.grade-mark.grade-d { background: color-mix(in srgb, var(--tone-failure) 22%, white); color: var(--tone-failure-ink); }

/* The four states a grade is shown in. Each hides everything but its own part,
   so the block never says two things at once -- a grade beside a "re-grade"
   offer is the thing this replaced. */
.grade-edited, .grade-working, .grade-skeleton { display: none; }
.grade[data-grade-state="none"] { display: none; }
.grade[data-grade-state="edited"] > .grade-mark,
.grade[data-grade-state="edited"] > .grade-note,
.grade[data-grade-state="grading"] > .grade-mark,
.grade[data-grade-state="grading"] > .grade-note,
.grade[data-grade-state="waiting"] > .grade-mark,
.grade[data-grade-state="waiting"] > .grade-note { display: none; }
.grade[data-grade-state="edited"] > .grade-edited,
.grade[data-grade-state="grading"] > .grade-working { display: inline-flex; align-items: center; gap: var(--sp-2); }
.grade[data-grade-state="waiting"] > .grade-skeleton { display: block; }

/* The offer that replaces a grade whose line has been rewritten. The old grade
   is gone rather than greyed: it was about words that are no longer there, and
   a caption saying so was easy to miss and gave nobody anything to press. */
.grade-edited { color: var(--ink-3); font-style: italic; }
.grade-edited > .btn { font-style: normal; margin-left: var(--sp-1); }

.spinner-sm { width: 13px; height: 13px; border-width: 2px; }

/* A grade that has not landed yet, shown as the shape of the answer rather
   than as an absence. A draft appears as soon as its objectives are settled,
   and grading them takes a few seconds longer; a row that simply had nothing
   under it read as a page that had finished. */
.grade-skeleton {
    height: 1.1em;
    width: min(60%, 26rem);
    border-radius: var(--radius-sm);
    /* Read against both backgrounds a grade sits on: the white card under an
       objective, and the sunken block under a key result. A sweep between the
       sunken surface and its hover state vanished entirely on the second. */
    background: linear-gradient(90deg,
        var(--surface-hover) 25%, var(--line-strong) 50%, var(--surface-hover) 75%);
    background-size: 300% 100%;
    animation: grade-skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes grade-skeleton-sweep {
    from { background-position: 150% 0; }
    to   { background-position: -50% 0; }
}
@media (prefers-reduced-motion: reduce) {
    .grade-skeleton { animation: none; background: var(--line); }
}

/* --- 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);
}

/* Several values the script needs, asked for together above it. Each field
   drops the frame it has on its own; the group carries one instead. */
.script-inputs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--sp-3) var(--sp-4);
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    margin-bottom: var(--sp-3);
}
.script-inputs .script-input {
    padding: 0;
    border: none;
    border-radius: 0;
    background: none;
}

/* Where to run the script and what to do with what it prints, in order. */
.setup-steps {
    margin: 0 0 var(--sp-3) 0;
    padding-left: var(--sp-5);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}
.setup-steps > li > .btn,
.setup-steps > li > .btn-copy { margin-right: var(--sp-2); }

/* A link standing in for a button, refused until the values are filled in. */
.btn.is-disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.setup-result {
    display: flex;
    align-items: flex-end;
    gap: var(--sp-3);
    flex-wrap: wrap;
    margin-top: var(--sp-4);
}
.setup-result .field { flex: 1 1 20rem; margin: 0; }

/* 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; }
.ladder-step[data-state="each"] .ladder-mark { color: var(--ink-3); }
.ladder-step[data-state="each"] .ladder-name { color: var(--ink-2); font-weight: 400; }

/* 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);
}

/* What a Variation that runs to completion printed when its demo ran. Kept to
   a scrolling box so a long report does not push the rest of the page away. */
.demo-output {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    max-height: 32rem;
    overflow: auto;
    background: var(--surface-sunken);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 0.75rem;
    margin: 0.5rem 0 1rem;
}

/* ---- Stacked weekly spend chart (Mendel admin) ------------------------------
   Bars are HTML, sized by the page's script from data attributes, as the meter
   is. The axis, the plot and the week labels share one grid so they line up
   without measuring anything. */
.spend-chart {
    display: grid;
    /* minmax(0, ...) so the no-wrap week labels cannot hold the plot wider
       than its card on a narrow screen. */
    grid-template-columns: 56px minmax(0, 1fr);
    grid-template-rows: 220px auto;
    column-gap: var(--sp-2);
    row-gap: var(--sp-1);
    margin: var(--sp-3) 0;
}
.spend-axis { position: relative; }
.spend-tick {
    position: absolute;
    right: 0;
    transform: translateY(50%);
    font-size: var(--text-xs);
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
}
.spend-plot {
    position: relative;
    display: flex;
    align-items: flex-end;
    gap: 6px;
    border-bottom: 1px solid var(--line-strong);
}
.spend-gridline {
    position: absolute;
    left: 0;
    right: 0;
    border-top: 1px solid var(--line);
    pointer-events: none;
}
.spend-week {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    outline: none;
}
.spend-week:focus-visible { box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
.spend-bar {
    display: flex;
    flex-direction: column-reverse;   /* the first series sits on the baseline */
    gap: 2px;                         /* a surface gap between stacked fills */
    border-radius: 4px 4px 0 0;
    overflow: hidden;
}
.spend-seg { display: block; min-height: 1px; }
.spend-plot:hover .spend-week:not(:hover) .spend-bar { opacity: 0.55; }
.spend-labels { display: flex; gap: 6px; }
.spend-label {
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-align: center;
    font-size: var(--text-xs);
    color: var(--ink-3);
    white-space: nowrap;
}
@media (max-width: 600px) {
    .spend-label:nth-child(even) { visibility: hidden; }
}
.spend-series-0 { background: var(--chart-other); }
.spend-series-1 { background: var(--chart-1); }
.spend-series-2 { background: var(--chart-2); }
.spend-series-3 { background: var(--chart-3); }
.spend-series-4 { background: var(--chart-4); }
.spend-series-5 { background: var(--chart-5); }
.spend-series-6 { background: var(--chart-6); }
.spend-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    font-size: var(--text-sm);
    color: var(--ink-2);
}
.spend-legend-item { display: inline-flex; align-items: center; gap: var(--sp-2); }
.spend-swatch {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    flex: none;
}
.spend-tip {
    position: absolute;
    z-index: 10;
    min-width: 200px;
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface-card);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-2);
    font-size: var(--text-sm);
    color: var(--ink-1);
    pointer-events: none;
}
.spend-tip-head { font-weight: 600; margin-bottom: var(--sp-1); }
.spend-tip-row { display: flex; align-items: center; gap: var(--sp-2); }
.spend-tip-value { margin-left: auto; font-variant-numeric: tabular-nums; }

/* ==== Strategy page: the Vision and Strategy Memory ======================= */

/* An entry's words sit at the top of its callout, as the title would. */
.strategy-memory-text { margin: 0 0 var(--sp-2); }
/* The form for a new entry follows the list, or the empty note, with room
   enough not to read as part of the last entry. */
.strategy-memory-new { margin-top: var(--sp-4); }
/* The Vision as read, its paragraphs kept as typed. */
.vision-text { white-space: pre-line; margin: 0 0 var(--sp-3); line-height: 1.55; }

/* Comparable outputs side by side (doc 48). Breaks out of the page measure:
   each column needs reading width, and there are as many as Variations. */
.full-bleed {
    width: 100vw;
    margin-left: calc(50% - 50vw);
    padding: 0 var(--sp-5);
    box-sizing: border-box;
}
.compare-columns {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(520px, 1fr);
    gap: var(--sp-4);
    overflow-x: auto;
    align-items: start;
}
.compare-column {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    min-width: 0;
    scroll-margin: var(--sp-4);
}
/* The column the reader came for, from one Variation's cell in the comparison. */
.compare-column:target { box-shadow: var(--focus-ring); border-color: var(--brand); }
.compare-column-head {
    position: sticky;
    top: 0;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-3);
    padding: var(--sp-3);
    border-bottom: 1px solid var(--line);
    background: var(--surface);
}
.compare-text {
    margin: 0;
    padding: var(--sp-3);
    font-size: var(--text-sm);
    line-height: 1.45;
    text-align: left;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ---- Grading on evidence (doc 50 §9) ------------------------------------
   The comparison is read across many Variations at once, so the page drops
   its usual width for it. The grades are grouped by what each criterion is
   graded on, and the results they rest on open beneath, aligned with the
   columns above and each scrolling on its own so the table stays in reach. */
.page:has(.compare-wide) { max-width: none; }
.compare tr.grade-group th {
    text-align: left;
    background: var(--surface-card);
    border-top: 1px solid var(--line-strong);
    padding-top: var(--sp-4);
    font-weight: 650;
    position: sticky;
    left: 0;
}
.grade-group-state { font-weight: 400; color: var(--ink-3); margin-left: var(--sp-2); }
.compare .grade { display: inline-flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; justify-content: center; }
.compare .help-tip-body { white-space: normal; text-align: left; }
.btn-link {
    padding: 0;
    border: 0;
    background: none;
    color: var(--brand);
    font: inherit;
    cursor: pointer;
    text-decoration: underline;
}
.compare tr.results-band-toggle td { text-align: left; }
.compare tr.results-band-toggle td > * + * { margin-left: var(--sp-3); }
.compare tr.results-band td { vertical-align: top; white-space: normal; text-align: left; padding: var(--sp-2); }
.results-scroll {
    max-height: 70vh;
    min-width: 360px;
    max-width: 640px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
}
.results-text {
    margin: 0;
    padding: var(--sp-1) var(--sp-3);
    font-size: var(--text-sm);
    line-height: 1.45;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.results-hl { border-left: 4px solid; margin: var(--sp-1) 0; }
.results-hl-good { background: var(--tone-success-bg); border-color: var(--tone-success-ink); }
.results-hl-bad { background: var(--tone-failure-bg); border-color: var(--tone-failure-ink); }
.results-hl-note { margin: 0; padding: 0 var(--sp-3) var(--sp-2); font-size: var(--text-xs); color: var(--ink-2); }
/* Another criterion's passage while one is chosen: still there, not shouting. */
.results-hl-muted { background: transparent; border-color: var(--line); }
.results-hl-muted .results-hl-note { display: none; }
