/* =============================================================================
   I'd cap that — the app

   Spec: docs/DESIGN-SYSTEM.md. Every value comes from tokens.css.
   The whole design is a flat colour field, big type, and two line states.
   If you are typing a hex, a px or a cubic-bezier in here, stop.
   ============================================================================= */

@layer reset, layout, components, state;

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

  html, body { height: 100%; }

  body {
    margin: 0;
    background: var(--field);
    color: var(--live);
    font-family: var(--font-ui);
    font-variant-numeric: tabular-nums;
    -webkit-font-smoothing: antialiased;
    overscroll-behavior-y: none;
    overflow: hidden;          /* the transcript scrolls, the page never does */
    /* The field taking a verdict's colour. The one slow transition here. */
    transition: background-color var(--dur-field) var(--ease);
  }

  button, input { font: inherit; color: inherit; }
  button { cursor: pointer; }

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

  ::selection { background: color-mix(in oklab, var(--wash) 30%, transparent); }
}

@layer layout {
  .app {
    /* dvh, never vh — vh hides the dock on iOS.
       height, not min-height, and minmax(0, 1fr) rather than 1fr: a bare 1fr
       row has an automatic minimum of its content size, so the transcript grew
       the whole page instead of scrolling inside it. Combined with the old
       justify-content:flex-end that put every earlier line above the top of
       an unscrollable box, and a whole conversation became unreachable. */
    height: 100dvh;
    min-height: 100dvh;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    max-width: var(--measure);
    margin-inline: auto;
    padding-inline: calc(var(--gutter) + var(--safe-left))
                    calc(var(--gutter) + var(--safe-right));
  }

  .topbar {
    display: flex;
    align-items: center;
    gap: var(--sp-4);
    padding-top: calc(var(--sp-6) + var(--safe-top));
    padding-bottom: var(--sp-4);
  }

  /* Two rows, not a wrapping flex line. The input used to take a whole row of
     its own and then 22px of padding stacked on top of the home-indicator
     inset, which on a phone left most of a fifth of the screen doing nothing.
     max() rather than +: the inset already clears the home indicator, so
     adding our own spacing on top of it doubles the gap. */
  .foot {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--sp-2) var(--sp-3);
    align-items: center;
    padding: var(--sp-3) 0 max(var(--sp-3), var(--safe-bottom));
    border-top: 1px solid var(--hair);
  }
  .foot .status { grid-column: 1 / -1; }
}

@layer components {

  /* --- the mark -----------------------------------------------------------
     Three bars in the three verdict colours, and the name set in the same
     three. The bars are a tally: this is a product that counts things, and
     the mark says so before a word is read. */
  .logo {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    user-select: none;
  }
  .bars {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: calc(var(--fs-logo) * 0.92);
  }
  .bars i {
    display: block;
    width: 4px;
    border-radius: 2px;
  }
  .bars i:nth-child(1) { height: 58%;  background: var(--v-green); }
  .bars i:nth-child(2) { height: 100%; background: var(--v-red); }
  .bars i:nth-child(3) { height: 76%;  background: var(--v-amber); }

  .wm {
    font-family: var(--font-display);
    font-size: var(--fs-logo);
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1;
    white-space: nowrap;
  }
  .wm b { font-weight: 700; }
  .wm .w1 { color: var(--v-green); }
  .wm .w2 { color: var(--v-red); }
  .wm .w3 { color: var(--v-amber); }

  /* The tally reads as numerals, not as text. It is the one glance that tells
     the story of a whole conversation. */
  .tally { margin-left: auto; display: flex; gap: var(--sp-5); align-items: baseline; }
  .tly {
    display: flex; align-items: baseline; gap: var(--sp-2);
    font-size: var(--fs-count-lbl); font-weight: var(--fw-semi);
    letter-spacing: var(--ls-label); text-transform: uppercase;
    color: var(--faint);
  }
  .tly b {
    font-family: var(--font-display);
    font-size: var(--fs-count); font-weight: 700;
    letter-spacing: -0.03em; line-height: 1;
  }
  .tly[data-k="cap"] b   { color: var(--v-red); }
  .tly[data-k="nocap"] b { color: var(--v-green); }
  .tly[data-zero="1"] b  { color: var(--past); }

  .budget {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--faint);
  }
  .budget[data-out="1"] { color: var(--v-red); }

  /* --- the transcript ---------------------------------------------------- */
  .lyr {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    overflow-anchor: auto;
    display: flex;
    flex-direction: column;
    gap: var(--line-gap);
    padding: var(--sp-7) 0 var(--sp-5);
    /* Lines dissolve into the field at the top rather than being cut off. */
    mask-image: linear-gradient(to bottom, transparent 0, #000 var(--sp-8), #000 100%);
    scrollbar-width: none;
  }
  .lyr::-webkit-scrollbar { display: none; }

  /* Lines sit at the bottom and grow upward, WITHOUT justify-content:flex-end.
     In a scroll container that keeps the overflow above the top edge
     unreachable -- scrollTop cannot go negative -- so once the transcript is
     longer than the screen you can never scroll back to the start of your own
     conversation. An auto margin on a zero-height first item does the same
     job and collapses to nothing the moment the content overflows. */
  .lyr::before { content: ""; margin-block-start: auto; }

  .line {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--fs-line);
    font-weight: var(--fw-line);
    line-height: var(--lh-line);
    letter-spacing: var(--ls-line);
    text-wrap: pretty;
    color: var(--past);
    transition: color var(--dur-line) var(--ease),
                opacity 380ms var(--ease),
                transform 380ms var(--ease);
  }
  /* A line arrives rather than appears. Removed on the next frame, so the
     transition above carries it in. */
  .line.enter { opacity: 0; transform: translateY(10px); }
  /* The lit states. A line stays lit while it still has work in it, and
     lights back up when a late verdict finally lands on it. */
  .line.live, .line.held, .line.relit { color: var(--live); }

  .interim { color: var(--faint); }
  .interim::after {
    content: "";
    display: inline-block;
    width: 0.06em; height: 0.82em;
    margin-left: 0.08em;
    vertical-align: -0.06em;
    background: currentColor;
    animation: blink 1100ms steps(2) infinite;
  }
  @keyframes blink { 50% { opacity: 0; } }

  .skip {
    display: inline-block;
    margin-left: var(--sp-3);
    padding: var(--sp-1) var(--sp-3);
    border: 1px solid var(--hair);
    border-radius: var(--r-pill);
    font-family: var(--font-ui);
    font-size: var(--fs-label);
    font-weight: var(--fw-med);
    letter-spacing: var(--ls-label);
    text-transform: uppercase;
    color: var(--sunk);
    vertical-align: middle;
  }

  /* --- a claim in the words ---------------------------------------------- */
  .mark {
    position: relative;
    color: inherit;
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    letter-spacing: inherit;
    -webkit-tap-highlight-color: transparent;
  }
  /* A 44px tap target without touching the line height. */
  .mark::after { content: ""; position: absolute; inset: -10px -4px; }

  /* The global focus ring draws a large rounded box around a whole clause,
     which on a 34px line is the loudest thing on screen. Tight, thin, and in
     the verdict's own colour instead. */
  .mark:focus-visible {
    outline: 1.5px solid currentColor;
    outline-offset: 2px;
    border-radius: 3px;
  }
  .mark[data-v]:focus-visible { outline-color: var(--vc); }
  .mark .txt { position: relative; }

  /* Checking: a marker dragged over the words, over and over. */
  .mark.checking .txt {
    background-image: linear-gradient(100deg,
      transparent 0%,
      color-mix(in oklab, var(--wash) 34%, transparent) 42%,
      color-mix(in oklab, var(--wash) 34%, transparent) 58%,
      transparent 100%);
    background-size: 220% 100%;
    background-repeat: no-repeat;
    box-shadow: inset 0 -2px 0 color-mix(in oklab, var(--wash) 62%, transparent);
    animation: sweep var(--dur-sweep) linear infinite;
  }
  @keyframes sweep {
    from { background-position: -110% 0; }
    to   { background-position:  210% 0; }
  }

  .mark[data-v] .txt {
    border-bottom: 2px var(--vline) var(--vc);
    transition: border-color var(--dur-base) var(--ease),
                color var(--dur-base) var(--ease);
  }
  /* The two loud ones colour the words themselves; the quiet four do not. */
  .mark[data-v="CONTRADICTED"] .txt {
    color: var(--v-red);
    text-decoration: line-through;
    text-decoration-thickness: 2px;
    text-decoration-color: color-mix(in oklab, var(--v-red) 70%, transparent);
  }
  .mark[data-v="SUPPORTED"] .txt { color: var(--v-green); }

  /* Punctuation pulled inside the mark so the label never splits a sentence
     from its full stop. It is never struck through and never recoloured. */
  .punct { color: var(--live); text-decoration: none; }
  .line:not(.live):not(.held):not(.relit) .punct { color: inherit; }

  .tag {
    display: inline-block;
    margin-left: 0.5em;
    vertical-align: 0.14em;
    font-family: var(--font-ui);
    font-size: var(--fs-tag);
    font-weight: var(--fw-line);
    letter-spacing: var(--ls-label);
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--vc);
    animation: tagin var(--dur-base) var(--ease) both;
  }
  /* The ladder, in the slot the verdict will land in. Never implies a rate:
     the count goes up, nothing fills, nothing predicts an end. */
  .tag.work { color: var(--wash); font-weight: var(--fw-semi); }

  /* A claim that could not be checked is not a verdict and must never look
     like one -- the six verdict strings are fixed and this is not among them. */
  .mark[data-err] .txt {
    border-bottom: 2px dotted var(--faint);
    color: inherit;
  }
  .mark[data-err] .tag { color: var(--faint); }
  @keyframes tagin {
    from { opacity: 0; transform: translateY(-3px); }
    to   { opacity: 1; transform: none; }
  }

  /* --- the foot: status, mic, typed claim -------------------------------- */
  .status {
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    font-family: var(--font-mono);
    font-size: var(--fs-status);
    color: var(--faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .rotor { color: color-mix(in oklab, var(--wash) 78%, var(--live)); }
  .status[data-busy="0"] .rotor { color: var(--sunk); }

  .listen {
    display: flex; align-items: center; gap: var(--sp-3);
    min-height: 52px;
    padding: 0 var(--sp-6);
    border: 2px solid var(--past);
    border-radius: var(--r-pill);
    background: none;
    color: var(--live);
    font-size: var(--fs-body);
    font-weight: var(--fw-semi);
    letter-spacing: var(--ls-label);
    text-transform: uppercase;
    transition: border-color var(--dur-fast) var(--ease),
                background-color var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease);
  }
  .listen:hover { border-color: var(--past); }
  .listen .dot {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--faint);
  }
  .app:has(.cold:not([hidden])) .listen {
    background: var(--live);
    color: var(--field);
    border-color: var(--live);
  }
  .app:has(.cold:not([hidden])) .listen .dot { background: var(--v-red); }

  .listen.starting { border-style: dashed; border-color: var(--past); color: var(--past); }
  .listen.starting .dot { animation: pulse var(--dur-base) var(--ease-soft) infinite; }
  .listen:disabled { cursor: progress; }
  .listen.on { background: var(--live); color: var(--field); border-color: var(--live); }
  .listen.on .dot { background: var(--v-red); animation: pulse var(--dur-sweep) var(--ease-soft) infinite; }
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

  .say {
    min-width: 0;
    min-height: var(--tap);
    padding: 0 var(--sp-5);
    border: 1px solid var(--hair);
    border-radius: var(--r-md);
    background: color-mix(in oklab, var(--field) 92%, var(--live));
    font-size: var(--fs-input);   /* 17px — never below 16 */
    caret-color: var(--wash);
    transition: border-color var(--dur-fast) var(--ease);
  }
  .say::placeholder { color: var(--sunk); }
  .say:focus { outline: none; border-color: var(--past); }

  /* Sound is off by default and rarely touched, so it sits in the top bar
     rather than taking a third of the control row on a phone. */
  .mute {
    width: 30px; height: 30px;
    display: grid; place-items: center;
    border: 1px solid transparent;
    border-radius: var(--r-pill);
    background: none;
    font-size: 15px; line-height: 1;
    color: var(--sunk);
  }
  .mute[aria-pressed="true"] { color: var(--live); border-color: var(--hair); }

  /* --- cold start: the screen a stranger lands on ------------------------ */
  /* The landing screen IS the explanation. A judge opening this cold on their
     own phone has about ten seconds of patience and nobody to ask, so there is
     nothing to dismiss and nothing between them and trying it. It disappears
     the moment anything is said or typed. */
  .cold { display: grid; gap: var(--sp-6); padding-bottom: var(--sp-6); }

  /* Transcript lines grow up from the bottom, but the landing screen is not a
     transcript -- bottom-aligning it leaves half an empty phone above the one
     thing a new visitor is supposed to read. Centre it while it is showing. */
  .lyr.empty::before { margin-block-start: 0; }
  .lyr.empty .cold { margin-block: auto; }
  .cold h2 {
    margin: 0;
    font-family: var(--font-display);
    font-size: clamp(24px, 5.6vw, 30px);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.022em;
    color: var(--live);
    text-wrap: balance;
    max-width: 20ch;
  }
  .steps { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--sp-4); }
  .steps li {
    display: grid;
    grid-template-columns: 1.6em 1fr;
    gap: var(--sp-3);
    align-items: baseline;
    font-size: clamp(16px, 4.2vw, 18px);
    line-height: 1.45;
    color: var(--past);
  }
  .steps .n {
    font-family: var(--font-display);
    font-size: clamp(16px, 4.2vw, 18px);
    font-weight: 700;
    color: var(--sunk);
    font-variant-numeric: tabular-nums;
  }
  .steps b { color: var(--live); font-weight: var(--fw-semi); }
  .steps b.cap { color: var(--v-red); }

  .egs { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
  .eg {
    min-height: var(--tap);
    padding: var(--sp-3) var(--sp-5);
    border: 1px solid var(--hair);
    border-radius: var(--r-pill);
    background: none;
    font-size: var(--fs-body);
    color: var(--past);
    text-align: left;
    transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
  }
  .eg:hover { color: var(--live); border-color: var(--past); }
  .cold .hint { margin: 0; font-size: var(--fs-body); line-height: 1.6; color: var(--sunk); max-width: 46ch; }

  .cold { position: relative; }

  /* Dismissed only when someone asks. It used to vanish the moment a claim
     arrived, which took the instructions away from the person still reading
     them. */
  .cold-x {
    position: absolute; top: 0; right: 0;
    width: var(--tap); height: var(--tap);
    display: grid; place-items: center;
    border: 1px solid var(--hair);
    border-radius: var(--r-pill);
    background: none;
    color: var(--past);
    font-size: 15px;
  }
  .cold-x:hover { color: var(--live); border-color: var(--past); }

  /* An example says what it expects to come back, on hover and on focus. The
     colour is the point of the app stated before anything has happened. */
  .eg[data-v] { transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease); }
  .eg[data-v="CONTRADICTED"]:hover,
  .eg[data-v="CONTRADICTED"]:focus-visible { color: var(--v-red); border-color: var(--v-red); }
  .eg[data-v="SUPPORTED"]:hover,
  .eg[data-v="SUPPORTED"]:focus-visible { color: var(--v-green); border-color: var(--v-green); }
  .eg[data-v="PARTIALLY_SUPPORTED"]:hover,
  .eg[data-v="PARTIALLY_SUPPORTED"]:focus-visible { color: var(--v-amber); border-color: var(--v-amber); }

  /* Anchored to the button, not to the end of the text. Inside the scrolling
     content it landed wherever the instructions happened to stop, which was
     nowhere near the thing it was pointing at. */
  .rec-wrap { position: relative; display: inline-flex; }
  .point {
    position: absolute;
    bottom: calc(100% + 10px);
    left: 50%;
    transform: translateX(-50%);
    pointer-events: none;
    line-height: 1;
  }
  .point i {
    display: block;
    font-style: normal;
    font-size: 26px;
    color: var(--live);
    animation: nudge 1400ms var(--ease-soft) infinite;
  }
  @keyframes nudge {
    0%, 100% { transform: translateY(0);   opacity: .4; }
    50%      { transform: translateY(7px); opacity: 1; }
  }

  /* --- the evidence card -------------------------------------------------- */
  .card {
    position: fixed;
    z-index: var(--z-card);
    right: calc(var(--gutter) + var(--safe-right));
    bottom: calc(var(--sp-8) + var(--sp-7) + var(--safe-bottom));
    width: min(390px, calc(100vw - 2 * var(--gutter)));
    max-height: min(62dvh, 560px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--sp-7) var(--sp-6) var(--sp-6);
    border-radius: var(--r-xl);

    /* The card sits on a lighter surface than the field, so it re-derives the
       dim steps against ITS OWN background. Inheriting the field's steps left
       the summary, the quote and the hostnames a shade away from unreadable. */
    --surface: color-mix(in oklab, var(--field) 88%, var(--live));
    --past:  color-mix(in oklab, var(--surface) 30%, var(--live));
    --faint: color-mix(in oklab, var(--surface) 55%, var(--live));
    --sunk:  color-mix(in oklab, var(--surface) 70%, var(--live));
    --hair:  color-mix(in oklab, var(--surface) 84%, var(--live));
    background: var(--surface);
    border: 1px solid var(--hair);
    /* Two shadows: a wide soft one for depth and a tight one for the edge, so
       the card sits above the field rather than floating vaguely over it. */
    box-shadow: 0 24px 64px rgb(0 0 0 / 0.42), 0 2px 8px rgb(0 0 0 / 0.30);
    /* Entry and exit are animated from render.js, for the same reason the
       verdict moment is: one source of truth, and the end is an event. */
  }
  @media (max-width: 640px) {
    .card {
      left: calc(var(--gutter) + var(--safe-left));
      right: calc(var(--gutter) + var(--safe-right));
      width: auto;
    }
  }

  .card-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-5); }
  .card-v {
    font-size: var(--fs-tag); font-weight: var(--fw-line);
    letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--vc);
  }
  .card-ms { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-label); color: var(--sunk); }
  .card-x {
    margin-left: auto;
    border: 1px solid transparent; background: none; color: var(--faint);
    width: 30px; height: 30px; border-radius: var(--r-pill);
    display: grid; place-items: center; font-size: 14px;
    transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
  }
  .card-x:hover { color: var(--live); border-color: var(--hair); }

  .card h3 {
    margin: 0 0 var(--sp-5);
    font-size: 17px; font-weight: var(--fw-semi);
    line-height: 1.38; letter-spacing: -0.012em;
  }
  .card p { margin: 0 0 var(--sp-5); font-size: var(--fs-body); line-height: 1.62; color: var(--past); }

  /* The claim line is the field. It reads as text until you reach for it. */
  .claim-line {
    cursor: text;
    border-radius: var(--r-sm);
    margin-inline: calc(-1 * var(--sp-3));
    padding-inline: var(--sp-3);
    transition: background-color var(--dur-fast) var(--ease);
  }
  .claim-line:hover { background: color-mix(in oklab, var(--surface) 88%, var(--live)); }
  .claim-line:focus-visible { outline: 2px solid var(--past); outline-offset: 1px; }

  .claim-edit { margin: 0 0 var(--sp-5); }
  .claim-edit textarea {
    display: block;
    width: 100%;
    resize: none;
    overflow: hidden;
    margin-inline: calc(-1 * var(--sp-3));
    padding: var(--sp-3);
    width: calc(100% + var(--sp-3) * 2);
    border: 1px solid var(--past);
    border-radius: var(--r-md);
    background: color-mix(in oklab, var(--surface) 82%, var(--live));
    color: var(--live);
    font-family: inherit;
    font-size: 17px;
    font-weight: var(--fw-semi);
    line-height: 1.38;
    letter-spacing: -0.012em;
  }
  .claim-edit textarea:focus { outline: none; border-color: var(--live); }
  .edit-hint {
    margin: var(--sp-3) 0 0 !important;
    font-size: var(--fs-label);
    color: var(--sunk) !important;
    letter-spacing: 0.02em;
  }

  /* What was said, when it is not what was checked. The explanation for a
     wrong verdict, and the record we are not allowed to quietly discard. */
  .heard {
    margin: calc(-1 * var(--sp-3)) 0 var(--sp-5) !important;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    line-height: 1.5;
    color: var(--sunk) !important;
  }

  .prov {
    display: inline-block; margin-bottom: var(--sp-4);
    padding: var(--sp-1) var(--sp-3);
    border: 1px dashed var(--vc); border-radius: var(--r-sm);
    font-size: var(--fs-label); font-weight: var(--fw-semi);
    letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--vc);
  }

  .fix {
    margin: 0 0 var(--sp-5);
    padding: var(--sp-4) var(--sp-5);
    border-left: 3px solid var(--v-amber);
    border-radius: 0 var(--r-md) var(--r-md) 0;
    background: color-mix(in oklab, var(--field) 78%, var(--live));
    font-size: var(--fs-body); font-weight: var(--fw-med); line-height: 1.5;
  }
  .fix span { color: var(--faint); font-weight: 400; }

  .src {
    display: flex; align-items: center; gap: var(--sp-3);
    min-height: var(--tap);
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-5);
    border-radius: var(--r-md);
    transition: background-color var(--dur-fast) var(--ease);
    background: color-mix(in oklab, var(--field) 82%, var(--live));
    font-family: var(--font-mono); font-size: var(--fs-label);
    color: var(--past); text-decoration: none;
  }
  .src:hover { background: color-mix(in oklab, var(--field) 74%, var(--live)); }
  .src .host { color: var(--past); }

  /* Confidence: a filled-bar count and nothing else. Never green/amber/red —
     a green "high confidence" beside a red ABSOLUTE CAP is the most confusing
     thing this UI could show. Confidence and verdict may disagree. */
  .conf { display: inline-flex; align-items: center; gap: var(--sp-2);
          font-size: var(--fs-label); font-weight: var(--fw-semi);
          letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--faint); }
  .conf i { width: 4px; height: 12px; border-radius: 1px; background: var(--hair); }
  .conf[data-level="high"]   i:nth-child(-n+3),
  .conf[data-level="medium"] i:nth-child(-n+2),
  .conf[data-level="low"]    i:nth-child(-n+1) { background: var(--past); }

  .jump {
    /* Outside the scroll container: inside it, the pill would add to
       scrollHeight and fight the very measurement that positions it. */
    position: fixed;
    z-index: var(--z-bar);
    right: calc(var(--gutter) + var(--safe-right));
    bottom: calc(var(--sp-8) + var(--sp-7) + var(--safe-bottom));
    padding: var(--sp-3) var(--sp-5);
    border: 1px solid var(--hair);
    border-radius: var(--r-pill);
    background: color-mix(in oklab, var(--field) 86%, var(--live));
    font-family: var(--font-ui);
    font-size: var(--fs-label);
    font-weight: var(--fw-semi);
    letter-spacing: var(--ls-label);
    text-transform: uppercase;
    color: var(--past);
  }
  .jump[data-new="cap"] { border-color: var(--v-red); color: var(--v-red); }

  .card-foot { display: flex; align-items: center; gap: var(--sp-4);
               margin-top: var(--sp-5); padding-top: var(--sp-5);
               border-top: 1px solid var(--hair); flex-wrap: wrap; }
  .card-meta { font-family: var(--font-mono); font-size: var(--fs-label); color: var(--sunk); }

  /* --- failure must be loud ----------------------------------------------
     A dead transcription socket must never look like nobody talking. */
  .alarm {
    position: fixed;
    z-index: var(--z-alarm);
    inset-inline: 0;
    top: 0;
    padding: calc(var(--sp-4) + var(--safe-top)) var(--gutter) var(--sp-4);
    background: var(--alarm-fill);
    border-bottom: 2px solid var(--v-red);
    color: var(--live);
    font-size: var(--fs-body);
    font-weight: var(--fw-med);
    display: flex;
    align-items: center;
    gap: var(--sp-3);
  }
  .alarm b { color: var(--v-red); letter-spacing: var(--ls-label); text-transform: uppercase;
             font-size: var(--fs-label); font-weight: var(--fw-line); }
}

@layer components {
  /* The verdict, over the field, then gone.
     It sits above the transcript and below nothing -- it never takes a click,
     never reflows a line, and never waits for the reader. */
  .moment {
    position: fixed;
    inset-inline: 0;
    top: 50%;
    /* Below the card. A verdict landing while someone is reading the evidence
       must not paint over the thing they opened. */
    z-index: var(--z-moment);
    pointer-events: none;
    text-align: center;
    padding-inline: var(--gutter);
    font-family: var(--font-display);
    font-weight: 800;
    letter-spacing: -0.035em;
    line-height: 0.95;
    text-wrap: balance;
    opacity: 0;
  }
  .moment[data-loud="1"] { font-size: clamp(46px, 17vw, 104px); }
  .moment[data-loud="0"] {
    font-size: clamp(26px, 8vw, 52px);
    letter-spacing: -0.02em;
    color: var(--faint);
  }
  .moment[data-verdict="CONTRADICTED"] { color: var(--v-red); }
  .moment[data-verdict="SUPPORTED"]    { color: var(--v-green); }

  /* The moment is animated from render.js with the Web Animations API, not
     from here: one source of truth for its opacity, and its end is an event
     rather than a timeout hoping to agree with a stylesheet. */
  .moment { transform: translateY(-50%); }
}

@layer state {
  /* The field takes the verdict. The one moment the whole screen moves. */
  body[data-flash="CONTRADICTED"] { background-color: var(--flash-red); }
  body[data-flash="SUPPORTED"]    { background-color: var(--flash-green); }

  [hidden] { display: none !important; }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 1ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 1ms !important;
    }
    /* The field still changes — it just changes instantly rather than moving.
       Removing the signal entirely would remove the verdict. */
  }
}
