/* icd-onsite — the Klinik design system.
 *
 * Hand-written, no build step, no CDN: a hospital network or a demo laptop may
 * resolve nothing at all (docs/ONSITE_PLAN.md §4). Fonts are vendored next to
 * this file and served from the binary.
 *
 * Colour tokens are §4 verbatim. Anything derived from the artboards but not
 * named in the plan lives in its own block below, so the canonical set stays
 * recognisable.
 */

/* ---------------------------------------------------------------- fonts -- */

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/IBMPlexSans-Regular.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/IBMPlexSans-Medium.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/IBMPlexSans-SemiBold.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/IBMPlexMono-Regular.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/static/fonts/IBMPlexMono-Medium.woff2') format('woff2');
}

/* --------------------------------------------------------------- tokens -- */

:root {
  /* Klinik palette — ONSITE_PLAN.md §4. */
  --ground: #FAFAF8;
  --surface: #FFFFFF;
  --border: #E4E3DE;
  --ink: #2C3138;
  --muted: #6E7178;
  --accent: #2E6E8E;
  --accent-soft: #EAF1F5;
  --green: #3F7A55;
  --amber: #9A6B1F;
  --error: #B04A38;
  --error-soft: #FAEDEA;
  --error-border: #EBD5CF;

  /* Derived from the artboards, not named in §4. */
  --border-faint: #EFEEEA;
  --border-hair: #F5F4F1;
  --border-dashed: #E0DFDA;
  --ink-soft: #4A4F57;
  --trail-line: #EAE9E5;
  --trail-dot: #CFD3D8;
  --amber-soft: #FBF6EC;
  --amber-border: #E8DCC4;
  --surface-sunk: #FCFCFA;
  --dim: #9CA0A7;
  --placeholder: #B2B6BC;
  --accent-border: #D6E3EB;
  --accent-strong: #1F4F68;

  /* Type. Real fallbacks: the page must be legible before the woff2 lands, and
     stay legible if it never does. */
  --font-sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Geometry — cards 10px, buttons 7-8px (§4). */
  --radius-card: 10px;
  --radius-button: 7px;
  --radius-chip: 6px;
}

/* ---------------------------------------------------------------- reset -- */

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

html {
  -webkit-text-size-adjust: 100%;
}

/* The browser's own `[hidden] { display: none }` is a UA rule, so any author
   `display:` beats it — and several components here set `display: flex`. Without
   this, hiding an element with the `hidden` attribute silently does nothing. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
}

h1,
h2,
h3,
p {
  margin: 0;
}

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

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

code,
kbd,
samp,
pre,
.mono {
  font-family: var(--font-mono);
}

/* ICD codes are always mono: 0/O and 1/l have to stay apart (§4). */
.code {
  font-family: var(--font-mono);
  font-weight: 500;
}

/* --------------------------------------------------------------- topbar -- */

.topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  height: 56px;
  padding: 0 24px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar__brand {
  display: flex;
  align-items: center;
  gap: 10px;
}

.topbar__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-chip);
  background: var(--accent);
  color: var(--surface);
}

.topbar__name {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.topbar__spacer,
/* The same "push the rest to the right" job, outside the topbar. */
.spacer {
  flex-grow: 1;
}

.chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  background: var(--ground);
  border: 1px solid var(--border);
  border-radius: var(--radius-chip);
  font-size: 12px;
}

.chip__label {
  color: var(--muted);
}

.chip__value {
  font-weight: 500;
}

.status {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  color: var(--muted);
}

.status__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--muted);
}

.status--ready .status__dot {
  background: var(--green);
}

.status--error .status__dot {
  background: var(--error);
}

/* ----------------------------------------------------------------- page -- */

.page {
  display: flex;
  justify-content: center;
  padding: 44px 40px;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 22px;
  width: 100%;
  max-width: 860px;
}

.page__title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 14px;
}

.page__title h1 {
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -0.015em;
}

.page__subtitle {
  font-size: 13.5px;
  color: var(--muted);
}

/* ----------------------------------------------------------------- card -- */

.card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}

.card--main .card__body {
  padding: 20px 22px 22px 22px;
}

.card--empty {
  border-style: dashed;
  border-color: var(--border-dashed);
}

.card__header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border-faint);
}

.card__meta {
  font-size: 12px;
  color: var(--dim);
}

.card__label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
}

.card__body {
  padding: 18px;
}

.card__footer {
  padding: 11px 18px;
  background: var(--surface-sunk);
  border-top: 1px solid var(--border-faint);
}

.placeholder {
  color: var(--placeholder);
  font-size: 14.5px;
  line-height: 1.6;
}

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

.note--warn {
  border-left: 2px solid var(--amber);
  padding-left: 9px;
}

/* --------------------------------------------------------------- button -- */

.button {
  padding: 11px 22px;
  border: 1px solid transparent;
  border-radius: var(--radius-button);
  background: var(--accent);
  color: var(--surface);
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
}

.button:hover {
  background: var(--accent-strong);
}

.button--quiet {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--accent);
}

.button--quiet:hover {
  background: var(--accent-border);
  color: var(--accent-strong);
}

.button:disabled {
  background: var(--border);
  color: var(--dim);
  cursor: default;
}

/* Anchors styled as buttons: `a` has no `cursor: pointer` reset to undo, but it
   does need the text-decoration and colour taken off. */
a.button {
  display: inline-block;
  text-decoration: none;
  color: var(--surface);
}

a.button:hover {
  color: var(--surface);
}

.link {
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
}

.link:hover {
  text-decoration: underline;
}

/* ----------------------------------------------------------------- form -- */

/* For labels that exist for screen readers but would repeat a visible heading. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.card__body--flush {
  padding: 0;
}

.textarea {
  display: block;
  width: 100%;
  min-height: 268px;
  padding: 18px;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-family: inherit;
  font-size: 14.5px;
  line-height: 1.6;
  resize: vertical;
}

.textarea::placeholder {
  color: var(--placeholder);
}

.textarea:focus {
  outline: 2px solid var(--accent-border);
  outline-offset: -2px;
}

.form-error {
  padding: 10px 14px;
  background: var(--error-soft);
  border: 1px solid var(--error-border);
  border-radius: var(--radius-button);
  color: var(--error);
  font-size: 13px;
  font-weight: 500;
}

.actions {
  display: flex;
  align-items: center;
  gap: 16px;
}

.hint {
  font-size: 12.5px;
  color: var(--dim);
}

.details {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}

.details__summary {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 16px 18px;
  cursor: pointer;
  list-style: none;
}

.details__summary::-webkit-details-marker {
  display: none;
}

/* The disclosure triangle from the artboard, drawn rather than vendored. */
.details__summary::before {
  content: '';
  width: 6px;
  height: 6px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(-45deg);
  transition: transform 120ms ease;
}

.details[open] .details__summary::before {
  transform: rotate(45deg);
}

.details__title {
  font-size: 13px;
  font-weight: 600;
}

.details__hint {
  font-size: 12.5px;
  color: var(--dim);
}

.details__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 0 18px 16px 18px;
}

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field__label {
  font-size: 12px;
  color: var(--muted);
}

.input {
  height: 36px;
  padding: 0 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-button);
  background: var(--surface-sunk);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 13px;
}

.input::placeholder {
  color: var(--placeholder);
}

/* The optional inputs below the composer. Two labelled groups rather than one
   flat grid: „bekannte Codes“ and „Aufenthalt“ answer different questions and
   only one of them is patient data. */
.extras__group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.extras__group + .extras__group {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.extras__label {
  font-size: 12.5px;
  font-weight: 600;
}

/* The group's own fields sit under its hint, not next to it. */
.extras__group .grid-3,
.extras__group .grid-4 {
  margin-top: 4px;
}

.grid-4 {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}

/* A select is not an input and inherits none of its box; match it so the row
   does not step. `appearance: none` also drops the platform arrow, which at this
   size reads as a misaligned glyph rather than a control. */
select.input {
  appearance: none;
  font-family: inherit;
  cursor: pointer;
}

/* A select has no `placeholder`, so its empty option is one — and has to look
   like one, or „keine Angabe“ reads as a chosen value. `:has` updates live as
   the selection changes, which an attribute selector could not; a browser
   without it just shows the text in the normal colour. */
select.input:has(option[value=""]:checked) {
  color: var(--placeholder);
}

/* `input[type=date]` ignores `placeholder` and draws its own localized mask
   (tt.mm.jjjj), in the same colour as a real value — so an empty date field
   reads as a filled one next to three actual placeholders. There is no selector
   for "a date input with no value" (an optional empty one is `:valid`), so the
   state is rendered as a class instead: server-side from the value, and kept up
   to date by the island. Without scripting the initial render is still right. */
.input--empty::-webkit-datetime-edit {
  color: var(--placeholder);
}

@media (max-width: 720px) {
  .grid-3,
  .grid-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The basis strip under the LKF figures: what the points were computed against. */
.lkf__basis {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
}

.lkf__basislabel {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.basis {
  font-size: 12.5px;
  color: var(--muted);
}

/* Assumed values are dimmed rather than coloured: they are not a fault, they
   are simply not the case's own. The tag is what states it. */
.basis--assumed {
  color: var(--dim);
}

.basis__tag {
  padding: 1px 5px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 10.5px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* Its own line under the values, not another chip in the row. */
.lkf__basisnote {
  flex-basis: 100%;
  font-size: 12px;
  color: var(--dim);
}

/* ------------------------------------------------------------- run head -- */

.runhead {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}

.runhead__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.runhead__title {
  font-size: 14.5px;
  font-weight: 600;
}

.runhead__sub {
  font-size: 12.5px;
  color: var(--muted);
}

.spinner {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border: 2px solid var(--accent-border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 900ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Respect a reduced-motion preference: the spinner is decoration, the steps
   below it carry the actual news. */
@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
  }
}

/* Where the wait is actually signalled. The spinner is a footnote; the letter
   is what the person is waiting to watch change, so it is the letter that goes
   quiet. An overlay would have hidden the one thing worth looking at. */
.card[aria-busy="true"] .textarea {
  opacity: 0.4;
  transition: opacity 120ms ease;
}

/* The footer-strip variant. The run screen's spinner sits next to a 15px
   heading; this one sits in a 12.5px strip and would tower over it. */
.spinner--inline {
  width: 14px;
  height: 14px;
  border-width: 2px;
}

.badge {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
}

.badge--done {
  background: var(--accent-soft);
  color: var(--accent);
}

.badge--failed {
  background: var(--error-soft);
  color: var(--error);
}

.elapsed {
  font-size: 13px;
  color: var(--muted);
  /* Digits must not shuffle the layout as the clock ticks. */
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------- trail -- */

/* Not a log of what happened: the set of things happening. Every line here is
   work still open, several at once, and a line leaves when its work is done —
   so there is no connecting spine and no "the last one is the current one".

   Everything below is motion, and motion has a job here beyond looking alive:
   the first letter after a fresh GPU spends over a minute on one step, and a
   page that sits perfectly still for that long reads as a hang. */

.trail {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.trail__step {
  position: relative;
  padding: 5px 0 5px 24px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
  /* `backwards` so a staggered step is invisible until its delay elapses
     rather than flashing at full opacity first. */
  animation: trail-in 320ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
  /* Collapsing rather than vanishing: `remove()` alone yanks every line below
     up by a row, and on a list that changes this often the jump is the thing
     the eye follows instead of the words. */
  max-height: 4rem;
  overflow: hidden;
  transition:
    max-height 260ms ease,
    padding 260ms ease,
    opacity 200ms ease,
    transform 260ms ease;
}

.trail__step--leaving {
  max-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  opacity: 0;
  transform: translateX(6px);
}

/* A dot, and a ring leaving it. The ring is what carries a step that sits for
   a minute: the dot alone is a bullet, the ring is a heartbeat. */
.trail__step::before,
.trail__step::after {
  content: '';
  position: absolute;
  left: 7px;
  top: 13px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

.trail__step::before {
  background: var(--accent);
}

.trail__step::after {
  border: 1px solid var(--accent);
  animation: trail-ring 1900ms ease-out infinite;
}

/* A line on its way out stops asking for attention. */
.trail__step--leaving::before {
  background: var(--trail-dot);
}

.trail__step--leaving::after {
  animation: none;
  opacity: 0;
}

/* A slow sheen across the words, the way a thing that is thinking looks.
   Guarded: without background-clip the text would be transparent and the step
   would be invisible, which is a worse page than one that does not shimmer. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .trail__step:not(.trail__step--leaving) .trail__text {
    background-image: linear-gradient(
      100deg,
      var(--ink) 0%,
      var(--ink) 38%,
      var(--accent) 50%,
      var(--ink) 62%,
      var(--ink) 100%
    );
    background-size: 260% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    animation: trail-sheen 3.4s ease-in-out infinite;
  }
}

@keyframes trail-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes trail-ring {
  0% {
    opacity: 0.5;
    transform: translate(-50%, -50%) scale(1);
  }
  70% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(2.8);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(2.8);
  }
}

@keyframes trail-sheen {
  from { background-position: 130% 0; }
  to   { background-position: -30% 0; }
}

/* A note is a fact, not work: it keeps the dot and loses everything that means
   "this is happening now". The Hauptdiagnose lands here the moment it is known,
   twenty seconds before the result page, so it earns a little more ink than the
   lines it sits among. */
.trail__step--note {
  color: var(--ink);
  font-weight: 500;
}

.trail__step--note::before {
  background: var(--green);
}

.trail__step--note::after {
  animation: none;
  opacity: 0;
}

.trail__step--note .trail__text {
  background-image: none;
  color: var(--ink);
  animation: none;
}

/* A hairline sweeping the top edge of the card, so the page is visibly working
   even in the gap between two steps — or during the one long step at the
   start, when nothing else on screen changes for a minute. */
.card--running {
  position: relative;
  overflow: hidden;
}

.card--running::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background-image: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 38% 100%;
  background-repeat: no-repeat;
  animation: card-sweep 2200ms cubic-bezier(0.6, 0, 0.4, 1) infinite;
}

@keyframes card-sweep {
  from { background-position: -40% 0; }
  to   { background-position: 140% 0; }
}

/* Someone who asked for less motion gets the words and the dot, and nothing
   that moves. The sheen has to give its colour back explicitly — a stopped
   gradient on transparent text is an empty line. */
@media (prefers-reduced-motion: reduce) {
  .trail__step,
  .trail__step::after,
  .card--running::before {
    animation: none;
  }

  .card--running::before {
    display: none;
  }

  .trail__step {
    transition: opacity 200ms ease;
  }

  .trail__step:not(.trail__step--leaving) .trail__text {
    background-image: none;
    color: var(--ink);
    animation: none;
  }
}

.trail__empty {
  font-size: 12.5px;
  color: var(--placeholder);
}

/* -------------------------------------------------------- found (live) -- */

/* The Nebendiagnosen as they arrive, above the trail of what is still running.
   Unlike a trail step this never leaves: it is an answer, and the same answer
   the result page shows — so it is the codelist's type, with the letter's own
   line under each code as the evidence for it. */

.found {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin: 0 0 18px;
  padding: 0;
  list-style: none;
}

.found__item {
  display: flex;
  flex-direction: column;
  gap: 3px;
  animation: trail-in 320ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
}

.found__code-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 14px;
}

.found__code-line .code {
  font-size: 16px;
}

/* The quoted diagnosis line, set apart by a rule rather than quotation marks:
   the text is the letter's, and a mark inside it would read as part of it. */
.found__quote {
  border-left: 2px solid var(--border);
  padding-left: 9px;
  font-size: 12.5px;
  color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  .found__item {
    animation: none;
  }
}

/* --------------------------------------------------------------- result -- */

/* `.duo` lived here: the side-by-side pair of Nebendiagnosen and MEL-Codes.
   The MEL card is gone until MEL coding is its own project, so Nebendiagnosen
   is a plain full-width card and the wrapper had nothing left to lay out. */

.code-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 16px;
}

.code--display {
  font-size: 32px;
  letter-spacing: -0.02em;
}

.code-desc {
  font-size: 16px;
  color: var(--ink-soft);
}

.pill {
  padding: 2px 9px;
  border-radius: 20px;
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
}

.codelist {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.codelist__item {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 14px;
}

.codelist__item .code {
  font-size: 16px;
}

.codelist__desc {
  font-size: 13.5px;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------------ lkf -- */

.lkf__figures {
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 18px 22px;
}

.lkf__figure {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.lkf__label {
  font-size: 12px;
  color: var(--muted);
}

.lkf__value {
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -0.02em;
}

.lkf__divider {
  width: 1px;
  height: 38px;
  background: var(--border-faint);
}

.lkf__counts {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
}

.count {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border: 1px solid var(--border-faint);
  border-radius: 20px;
  font-size: 12.5px;
  color: var(--muted);
}

.count--warn {
  background: var(--amber-soft);
  border-color: var(--amber-border);
  color: var(--amber);
  font-weight: 500;
}

.count--error {
  background: var(--error-soft);
  border-color: var(--error-border);
  color: var(--error);
  font-weight: 500;
}

.lkf__messages {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--border-faint);
  list-style: none;
}

.lkf__message {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 11px 22px;
  border-bottom: 1px solid var(--border-hair);
}

.lkf__message:last-child {
  border-bottom: 0;
}

/* The same pill as the totals above, at row scale — a row and the count it is
   part of should not be two different shapes. */
.count--sm {
  flex-shrink: 0;
  justify-content: center;
  min-width: 68px;
  padding: 2px 9px;
  font-size: 11.5px;
}

.lkf__code {
  flex-shrink: 0;
  width: 62px;
  font-size: 12px;
  color: var(--muted);
}

.lkf__text {
  font-size: 13px;
}

/* ---------------------------------------------------------------- panel -- */

.panel {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px 22px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}

.panel--error {
  border-color: var(--error-border);
}

.panel__title {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--error);
}

.panel__text {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--ink-soft);
}

/* The machine's own words. Wrapped, not clipped: a truncated error message is
   worth nothing to whoever has to report it. */
.detail-box {
  padding: 10px 13px;
  background: var(--surface-sunk);
  border: 1px solid var(--border-faint);
  border-radius: var(--radius-button);
  color: var(--dim);
  font-size: 11.5px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------- narrow -- */

@media (max-width: 720px) {
  .page {
    padding: 24px 16px;
  }

  .grid-3 {
    grid-template-columns: 1fr;
  }

  .lkf__figures {
    flex-wrap: wrap;
    gap: 20px;
  }
}

/* ------------------------------------------------------- anonymisierung -- */

/* The chip next to "Brieftext" that says whether the text in the box has been
   through the anonymizer. Three states, one shape, always in the same place —
   the point is that it is checkable at a glance, not that it is decorative. */
.state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--ground);
  color: var(--muted);
  font-size: 12px;
}

/* The green pair is new in the system: green existed only as ink (--green),
   never as a surface. */
.state--done {
  border-color: #CBE0D2;
  background: #EDF5F0;
  color: var(--green);
  font-weight: 500;
}

.state--error {
  border-color: var(--error-border);
  background: var(--error-soft);
  color: var(--error);
  font-weight: 500;
}

/* Same words as the plain chip, said louder: "nicht anonymisiert" is a neutral
   fact before you press the button and a warning after. */
.state--warn {
  border-color: var(--amber-border);
  background: var(--amber-soft);
  color: var(--amber);
  font-weight: 500;
}

.card--error {
  border-color: var(--error-border);
}

/* One step down from card--error, and the same idea: the card itself carries
   the state, so the warning is not one banner shouting on an otherwise calm
   screen. */
.card--warn {
  border-color: var(--amber-border);
}

/* The strip under the textarea. It carries the whole anonymization interaction,
   so it is a row with the explanation on the left and the actions on the
   right — in all three states, so nothing moves as the state changes. */
.anonbar {
  display: flex;
  align-items: center;
  gap: 11px;
}

.anonbar__icon {
  flex-shrink: 0;
}

.anonbar__text {
  font-size: 12.5px;
  color: var(--muted);
}

.anonbar__text strong {
  font-weight: 600;
  color: var(--ink);
}

.card__footer--done {
  background: var(--accent-soft);
  border-top-color: var(--accent-border);
}

.card__footer--error {
  background: var(--error-soft);
  border-top-color: var(--error-border);
}

.card__footer--warn {
  background: var(--amber-soft);
  border-top-color: var(--amber-border);
}

.anonbar__lines {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.anonbar__title {
  font-size: 13px;
  font-weight: 600;
  color: var(--error);
}

/* An outlined button: the anonymize and retry actions are deliberately not the
   filled accent, because "Kodierung starten" below them is, and there must be
   exactly one obvious next step on the screen. */
.button--outline {
  background: var(--surface);
  border-color: var(--accent);
  color: var(--accent);
  padding: 8px 15px;
  font-size: 13px;
}

.button--outline:hover {
  background: var(--accent-soft);
}

.button--outline-error {
  border-color: var(--error);
  color: var(--error);
}

.button--outline-error:hover {
  background: var(--surface);
  border-color: var(--error);
}

/* A submit button that has to read as a plain text action ("Rückgängig"): same
   look as .link, but it posts a form, so it must be a button. */
.link-button {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
}

.link-button:hover {
  color: var(--accent-strong);
  text-decoration: underline;
}

/* The one thing on this screen louder than the composer: the box saying it has
   not checked what is about to leave it. Amber, not red — nothing has gone
   wrong, and the way forward is still open. */
.warn-banner {
  display: flex;
  align-items: flex-start;
  gap: 13px;
  padding: 16px 18px;
  background: var(--amber-soft);
  border: 1px solid var(--amber-border);
  /* The one heavy rule on the screen. A left bar rather than a thicker border
     all round: it reads as a callout instead of as another card. */
  border-left: 4px solid var(--amber);
  border-radius: var(--radius-card);
  color: var(--amber);
}

.warn-banner__icon {
  flex-shrink: 0;
  margin-top: 3px;
}

.warn-banner__lines {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.warn-banner__title {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.warn-banner__text {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------ vergleich -- */

/* The two marks are the same chip in the two system tints: amber for "found
   here", petrol for "replaced by this". */
/* The negative inline margin is load-bearing: without it the chip's padding
   pushes the comma after a name away from it, and the letter reads as though it
   had a space there. This way the chip has room inside and takes none outside. */
.hit,
.ph {
  padding: 1px 4px;
  margin-inline: -4px;
  border-radius: 4px;
}

.hit {
  border: 1px solid var(--amber-border);
  background: var(--amber-soft);
  color: inherit;
}

.ph {
  border: 1px solid var(--accent-border);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 12.5px;
}

/* The coder's evidence in the letter beside the result: the stretch it quoted
   for a code, underlined rather than tinted, because a tint in this text means
   „ersetzt“ (`.ph`). The codes are on the title, so hovering says which. Set on
   `mark` to undo the browser's yellow. */
.ev {
  background: transparent;
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
  cursor: help;
}

/* Column width, like everything else on the screen. It was briefly wider —
   two panes want the room — but a block that is broader than the card above it
   reads as misaligned rather than as roomy, and the panes are legible at half
   of 860px: about 52 characters a line, inside the comfortable measure. The
   tighter gutter below buys some of the width back. */

.compare__foot {
  display: flex;
  align-items: center;
  gap: 14px;
  height: 56px;
  padding: 0 22px;
  border-top: 1px solid var(--border-faint);
}

.compare:not([open]) .legend {
  display: none;
}

.legend {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  color: var(--muted);
}

.legend__swatch {
  width: 11px;
  height: 11px;
  border-radius: 3px;
}

.legend__swatch--hit {
  border: 1px solid var(--amber-border);
  background: var(--amber-soft);
}

.legend__swatch--ph {
  border: 1px solid var(--accent-border);
  background: var(--accent-soft);
}

/* The column heads carry the actual argument of this screen: what stays here
   and what goes out. */
.compare__cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border-top: 1px solid var(--border-faint);
  border-bottom: 1px solid var(--border-faint);
  background: var(--surface-sunk);
}

.compare__col {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 13px 18px;
}

.compare__col:first-child {
  border-right: 1px solid var(--border);
}

.compare__colname {
  font-size: 13px;
  font-weight: 600;
}

.compare__colnote {
  font-size: 12px;
  color: var(--muted);
}

.compare__colnote--sent {
  color: var(--accent);
}

/* One grid row per line of the letter, both sides in the same row: a name and
   its placeholder wrap differently, and two independent columns would slide out
   of step further down a long letter. */
.compare__body {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start;
  /* Bounded and self-scrolling: this sits above „Kodierung starten“, and a long
     letter would otherwise push the primary action off the screen. */
  max-height: 54vh;
  padding: 22px 0;
  font-size: 14px;
  line-height: 1.7;
  overflow-y: auto;
}

.compare__cell {
  min-height: 1.7em;
  padding: 0 18px;
  overflow-wrap: anywhere;
}

.compare__cell--left {
  border-right: 1px solid var(--border);
}

/* Narrow keeps both panes. Stacking them would interleave the letter line by
   line, and putting one under the other whole would lose the pairing that is
   the point of the view; a tighter gutter is the honest trade. */
@media (max-width: 900px) {
  .compare__col {
    padding: 13px 12px;
  }

  .compare__cell {
    padding: 0 12px;
  }

  .compare__body {
    font-size: 13px;
  }
}

/* ------------------------------------------------------- dropped files -- */

/* The picker in the card header. Deliberately quiet: pasting is the ordinary
   way in and this is the other one, not the louder one. */
.filepick {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-chip);
  color: var(--muted);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}

.filepick:hover {
  border-color: var(--accent-border);
  background: var(--accent-soft);
  color: var(--accent-strong);
}

/* Keyboard focus lands on the input, which is visually hidden — so the label
   has to wear the ring on its behalf. */
.filepick:focus-within {
  outline: 2px solid var(--accent-border);
  outline-offset: 1px;
}

.dropzone {
  position: relative;
}

/* Only while a file is over the window. A permanent drop target would be a
   second empty state competing with the placeholder, and the placeholder
   already says a file can be dropped. */
.dropzone__hint {
  display: none;
  position: absolute;
  inset: 6px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  border: 1px dashed var(--accent);
  border-radius: var(--radius-card);
  background: var(--accent-soft);
  color: var(--accent-strong);
  /* The drop is handled on the document, so the overlay only has to be seen —
     letting pointer events through keeps it out of the way of the drag. */
  pointer-events: none;
}

.dropzone.is-dragging .dropzone__hint {
  display: flex;
}

.dropzone__icon {
  margin-bottom: 4px;
  color: var(--accent);
}

.dropzone__title {
  font-size: 14px;
  font-weight: 600;
}

.dropzone__text {
  font-size: 12.5px;
  color: var(--accent);
}

/* Two chips, right-aligned in the topbar. Stacked and tight rather than side by
   side: they are two answers to two questions, and reading them as one sentence
   was the old mistake. */
.statusbar {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
}

.statusbar .status {
  font-size: 11.5px;
}

/* Amber: the coding GPU is on its way up. Not an error — a wait. */
.status--warn .status__dot {
  background: var(--amber);
}

/* Where „Kodierung starten“ would be, when it must not be offered. Quieter than
   .warn-banner on purpose: that one is about a letter that could leak, this one
   is about a machine that is not up. Nothing here is anybody's mistake. */
.blocked {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  padding: 13px 15px;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  color: var(--amber);
}

.blocked__lines {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.blocked__title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.blocked__text {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
}

/* --------------------------------------------------------------- result -- */

/* Only ever rendered for a flag that is set, so it reads as a statement about
   the case rather than as a field that happens to be false. */
.pill--note {
  margin-top: 10px;
}

/* ---------------------------------------------------------------- login -- */

.login {
  max-width: 480px;
}

.login__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 18px 22px;
}

/* The logged-in person's name and the way out, at the right end of the topbar. */
.topbar__session {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* ------------------------------------------------- letter beside result -- */

/* The result page is the one screen wider than the column: the letter needs
   about 80 characters a line at the composer's size, and the result keeps the
   640px it has everywhere else. */
.stack--wide {
  max-width: 1360px;
}

.result-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 640px;
  gap: 22px;
  align-items: start;
}

/* The result sticks and scrolls by itself if the LKF messages outgrow the
   viewport; the letter does not, see the template. */
.result-pane {
  display: flex;
  flex-direction: column;
  gap: 22px;
  position: sticky;
  top: 24px;
  max-height: calc(100vh - 48px);
  overflow-y: auto;
}

.letter__body {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 22px 24px 28px;
  font-size: 14.5px;
  line-height: 1.65;
  color: var(--ink);
}

.letter__sec {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.letter__h {
  font-weight: 600;
}

.letter__sec--diagnosis {
  gap: 6px;
  padding: 12px 14px;
  background: var(--ground);
  border: 1px solid var(--border-soft, var(--border));
  border-radius: 8px;
}

/* One column, result first, nothing sticky: a sticking result would cover
   half a tablet's screen. */
@media (max-width: 1100px) {
  .result-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .result-pane {
    order: -1;
    position: static;
    max-height: none;
    overflow: visible;
  }
}
