/* Vissia web interface: one small stylesheet, no build step, no external font or icon
   fetch. Kept plain deliberately, see docs/architecture.md section 6.5: this is a set of
   list and form screens, not a product that needs a design system of its own yet.

   Written mobile first: every base rule targets a phone, and `min-width` queries widen
   the layout as room appears, never the reverse. Two exceptions read the element's own
   width instead of the window's, `@container` rather than `@media`, because what fits is
   decided by the box the element actually has, not by how wide the browser happens to be:
   the review viewer's image and field columns (.viewer-container) and the field detail
   list's label and value pairs (.field-detail). Everything else is one screen wide, so a
   window based breakpoint says the same thing a container query would. */

:root {
  color-scheme: light dark;
  --border: #d7dce2;
  --muted: #5b6572;
  --accent: #1f5eda;
  --danger: #b3261e;
  --surface: #ffffff;
  --surface-alt: #f4f6f8;
  --text: #16191d;

  /* Spacing scale. Every gap, padding and margin below is one of these, so the rhythm of
     the page stays consistent instead of accumulating one-off values screen by screen. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;

  /* Type scale. Five sizes cover every piece of text in the interface. */
  --text-xs: 0.75rem;
  --text-sm: 0.8rem;
  --text-md: 0.9rem;
  --text-lg: 1.1rem;
  --text-xl: 1.4rem;

  --radius-sm: 4px;
  --radius-md: 8px;

  /* The smallest a control an operator taps may be, in both dimensions. WCAG's own AAA
     target size guideline, and the one measurement in this file that is deliberately not
     a design choice. */
  --tap-min: 2.75rem;

  --content-max: 72rem;
  --font-sans: -apple-system, "Segoe UI", "PingFang TC", "Noto Sans TC", system-ui, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --border: #3a4048;
    --muted: #9aa4b2;
    --accent: #6ea2ff;
    --danger: #ff8b82;
    --surface: #14171a;
    --surface-alt: #1c2024;
    --text: #eef1f4;
  }
}

* {
  box-sizing: border-box;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--text);
  background: var(--surface);
  line-height: 1.5;
}

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

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------------------------
   Header. Brand and language switch share the first line; the session block (sign in
   form, or who is signed in) is given the full row's width, which forces it onto its own
   line in the wrapping flex container without needing a second markup structure for
   narrow screens. From 48rem, three columns of room appear and it rejoins that line.
   ------------------------------------------------------------------------------------ */

.site-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  background: var(--surface-alt);
}

.brand {
  font-weight: 700;
  text-decoration: none;
  font-size: var(--text-lg);
}

.lang-switch {
  display: flex;
  gap: var(--space-1);
  margin-inline-start: auto;
}

.lang-switch a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  text-decoration: none;
}

.lang-switch a[aria-current="true"] {
  font-weight: 700;
  background: var(--surface);
}

.session {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
}

.session form.signin {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
  width: 100%;
}

.session label {
  font-size: var(--text-sm);
  color: var(--muted);
}

.session input {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}

.who {
  font-size: var(--text-sm);
  color: var(--muted);
  text-transform: capitalize;
}

@media (min-width: 48rem) {
  .session {
    width: auto;
  }

  .session form.signin {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    width: auto;
  }

  .session input {
    width: auto;
    min-width: 9rem;
  }
}

/* ------------------------------------------------------------------------------------
   Page shell and type.
   ------------------------------------------------------------------------------------ */

main {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--space-5) var(--space-4) var(--space-8);
}

@media (min-width: 48rem) {
  main {
    padding-inline: var(--space-5);
  }
}

h1 {
  font-size: var(--text-xl);
  margin-bottom: var(--space-4);
}

h2 {
  font-size: var(--text-lg);
  margin-top: var(--space-6);
}

/* ------------------------------------------------------------------------------------
   Buttons and form feedback.
   ------------------------------------------------------------------------------------ */

button {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  border: 1px solid var(--accent);
  background: var(--accent);
  color: #fff;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-md);
}

button.secondary {
  background: transparent;
  color: var(--accent);
}

button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.form-error,
.form-success {
  width: 100%;
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
}

.form-error {
  color: var(--danger);
}

.form-success {
  color: var(--accent);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.actions a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-size: var(--text-md);
}

.actions a:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* ------------------------------------------------------------------------------------
   Tables. Below roughly 45rem seven columns cannot fit a phone, so a row renders as a
   card instead: the markup and the translated headers stay exactly the same, only their
   display changes, from `table` to a flex stack of grid cards, which is why this is one
   set of rules rather than two templates. Each `td` says where it sits in that card
   through a column class:

   - `col-heading` / `col-heading-end` sit together on the card's first line, the two
     facts an operator identifies a row by without opening it.
   - `col-meta` carries everything else that needs a label, which it reads from
     `data-label`, a value that came from the catalog through the template rather than a
     `content:` string here, so it translates.
   - `col-reasons` is the one column that was already exempt from truncation.
   - `col-action` becomes the full width control the task asked for.

   `thead` is not removed, only visually hidden the same way `.visually-hidden` hides
   anything else: a screen reader still has the column names `data-label` repeats on
   screen, and the table regains its header row once there is room for one. */

.table-scroll {
  border: none;
}

table {
  display: block;
  width: 100%;
  border-collapse: collapse;
}

.table-scroll thead {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

th {
  text-align: start;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-alt);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

tbody {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

tr {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: var(--space-3);
  row-gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-alt);
}

td {
  display: block;
  border: none;
  padding: 0;
  white-space: normal;
  max-width: none;
  overflow-wrap: anywhere;
  overflow: visible;
  text-overflow: clip;
}

td.col-heading {
  grid-column: 1;
  font-size: var(--text-lg);
  font-weight: 600;
}

td.col-heading-end {
  grid-column: 2;
  font-size: var(--text-lg);
  font-weight: 600;
  text-align: end;
  white-space: nowrap;
}

td.col-meta,
td.col-reasons,
td.col-action {
  grid-column: 1 / -1;
}

td.col-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-2);
  font-size: var(--text-sm);
}

td.col-meta::before {
  content: attr(data-label);
  color: var(--muted);
  font-weight: 600;
}

/* The reasons an operator reads before deciding whether to open a document at all: the
   one thing on the queue that must never be cut off mid word, on any layout. */
td.col-reasons {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

td.col-reasons::before {
  content: attr(data-label);
  display: block;
  color: var(--muted);
  font-weight: 600;
  font-size: var(--text-sm);
  margin-bottom: var(--space-1);
}

td.col-action a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}

.reason-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.reason-list li {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-md);
}

.status-chip {
  display: inline-block;
  padding: 0.15rem var(--space-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
}

.empty-state {
  padding: var(--space-5);
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  text-align: center;
}

/* A short explanation beside a heading, such as why a list of payment proofs sits under a
   balance figure: never the only carrier of a fact, always beside the data itself. */
.note {
  color: var(--muted);
  font-size: var(--text-sm);
  margin-top: calc(var(--space-1) * -1);
}

/* An item's internal reference, and the button that copies it in one gesture: it is keyed
   into the client's own spreadsheet by hand, so a mouse selection on text that may not fit
   its column is not good enough. See app/web/static/copy.js. */
.item-ref {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.item-ref code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  word-break: break-all;
}

.copy-button {
  min-height: auto;
  padding: 0.2rem var(--space-2);
  font-size: var(--text-xs);
  background: transparent;
  color: var(--accent);
}

/* A value the platform computed rather than read off the document: requirement 2, always
   distinguished from a read figure, never by colour alone. */
.derived-tag {
  display: inline-block;
  margin-inline-start: var(--space-2);
  font-size: var(--text-xs);
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: 999px;
  padding: 0.1rem var(--space-2);
}

@media (min-width: 45rem) {
  .table-scroll {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
  }

  table {
    display: table;
    min-width: 45rem;
  }

  .table-scroll thead {
    position: static;
    width: auto;
    height: auto;
    padding: 0;
    margin: 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
    border: 0;
  }

  tbody {
    display: table-row-group;
    gap: 0;
  }

  tr {
    display: table-row;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
  }

  tbody tr:not(:last-child) td {
    border-bottom: 1px solid var(--border);
  }

  td {
    display: table-cell;
    padding: var(--space-3) var(--space-4);
    white-space: nowrap;
    max-width: 22rem;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: baseline;
  }

  td.col-heading,
  td.col-heading-end,
  td.col-meta,
  td.col-reasons,
  td.col-action {
    font-size: inherit;
    font-weight: inherit;
  }

  td.col-heading {
    font-weight: 600;
  }

  td.col-meta {
    display: table-cell;
    gap: 0;
  }

  td.col-meta::before,
  td.col-reasons::before {
    content: none;
  }

  td.col-reasons {
    white-space: normal;
  }

  td.col-action a {
    display: inline;
    min-height: auto;
    border: none;
    padding: 0;
    color: var(--accent);
    font-weight: normal;
  }
}

/* ------------------------------------------------------------------------------------
   Fields read from a document, and the flagged field list, both on the review screen.
   Every control here is something an operator taps repeatedly while working a task, so
   this is the one list the task singled out for the touch minimum.
   ------------------------------------------------------------------------------------ */

.field-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field-list button {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap-min);
  text-align: start;
  background: var(--surface-alt);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-md);
}

.field-list button[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--surface);
}

/* "Fields read from this document": field path and settled value, laid out apart so the
   value, the thing an operator actually reads, does not get lost in a long field path. */
.field-list button.observed-field {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
}

.observed-field-path {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  color: var(--muted);
}

.observed-field-value {
  font-weight: 600;
  word-break: break-word;
}

/* ------------------------------------------------------------------------------------
   The review viewer. `.viewer-container` is the size container the two column switch
   below reads from: the scan and the fields go side by side once the container itself
   has room, whatever the window is doing around it, per the task's own instruction to
   prefer a container query here over a window one.
   ------------------------------------------------------------------------------------ */

.viewer-container {
  container-type: inline-size;
}

.viewer-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}

@container (min-width: 55rem) {
  .viewer-layout {
    /* A purchase invoice being read by a person needs the width: the image column takes
       roughly two thirds of the row instead of leaving half the space empty beside a
       squeezed scan. */
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  }
}

.viewer-image-wrap {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-alt);
  overflow: hidden;
}

/* Only the empty and placeholder states need centering and a floor on height. Once an
   image is shown, the wrap must have no box of its own beyond the image's rendered box:
   review_viewer.js positions every highlight as a percentage of #viewer-image-wrap, and
   that only lands on the field it belongs to when the wrap's box and the image's box are
   the same rectangle. */
.viewer-image-wrap[data-has-image="false"] {
  min-height: 20rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.viewer-image-wrap img {
  display: block;
  width: 100%;
  height: auto;
  /* `auto` before the ratio means the browser prefers the image's own natural ratio the
     instant it is known, so the wrap keeps tracking the image's true rendered box exactly
     as before, the one invariant the highlight math depends on. Before that, and only
     then, a scanned page is close enough to A4 portrait that reserving this much space
     stops the field list from jumping down the page once the file arrives. A page shaped
     differently still renders in full, just with one small adjustment instead of a jump
     from nothing. */
  aspect-ratio: auto 3 / 4;
}

.viewer-placeholder {
  color: var(--muted);
  font-size: var(--text-md);
  padding: var(--space-4);
  text-align: center;
}

.viewer-highlight {
  position: absolute;
  border: 2px solid var(--danger);
  background: color-mix(in srgb, var(--danger) 18%, transparent);
  pointer-events: none;
}

/* `.field-detail` holds a label and value list in both places it appears: the document's
   own metadata at the top of the review screen, and the detail of whichever flagged field
   is selected. Its own width is what decides whether label and value fit side by side,
   not the window's, since the second use sits in a column whose width depends on the
   viewer layout above rather than on the screen. */
.field-detail {
  container-type: inline-size;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-alt);
  font-size: var(--text-md);
}

.field-detail dl {
  display: block;
  margin: var(--space-2) 0 0;
}

.field-detail dt {
  color: var(--muted);
  margin-top: var(--space-2);
}

.field-detail dt:first-child {
  margin-top: 0;
}

.field-detail dd {
  margin: 0;
  word-break: break-word;
}

@container (min-width: 22rem) {
  .field-detail dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-1) var(--space-3);
  }

  .field-detail dt {
    margin-top: 0;
  }
}

/* ------------------------------------------------------------------------------------
   Upload and decide forms.
   ------------------------------------------------------------------------------------ */

.upload-form,
.decide-form,
.token-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 28rem;
}

.upload-form label,
.decide-form label,
.token-form label {
  font-size: var(--text-sm);
  color: var(--muted);
}

.upload-form input,
.upload-form select,
.decide-form input,
.decide-form select,
.token-form input {
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-md);
}

/* The one screen that ever shows a bearer credential, and only for the response of the
   POST that just minted it: boxed off from the rest of the page so it cannot be mistaken
   for an ordinary row, per app/web/routes.py's account_issue_token. */
.token-issued {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 40rem;
  margin-block: var(--space-4);
  padding: var(--space-4);
  border: 2px solid var(--danger);
  border-radius: var(--radius-md);
  background: var(--surface-alt);
}

.token-value {
  display: block;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

/* A revoked or expired token, greyed relative to an active one, never the only signal:
   the status text itself already names which one it is. */
.status-chip-muted {
  color: var(--muted);
  border-color: var(--border);
  background: var(--surface);
}

/* The field path is filled by clicking a field elsewhere on the page, never typed: see
   app/web/static/operator.js. Shaded so it reads as filled in rather than as an ordinary
   text box waiting for input. */
.decide-form input[readonly] {
  background: var(--surface-alt);
  color: var(--muted);
}
