/* No webfonts. IBM Plex was tried here and rejected on the look — the app stays
   on the system stacks, which also keeps it byte-identical offline with nothing
   to precache. Don't reintroduce a font without looking at it on a real scan
   first. */
:root{
  --paper: #f6f7f9;
  --panel: #ffffff;
  --ink: #0e1217;
  --muted: #626a77; /* 5.09:1 on --paper / 5.46:1 on white (WCAG AA, cool slate) */
  --line: #e6e8ec;
  --line-strong: #d3d7de;
  /* The drop zone's own edge. --line-strong measures 1.4:1 on --panel, which is
     fine for a hover state or a panel seam but not for the boundary of the one
     control this page exists for: WCAG 1.4.11 asks 3:1 of a UI component
     boundary, and on a bright office monitor the dashed edge simply vanished.
     3.02:1 on --panel. Keep it separate — --line-strong is deliberately soft
     where it is used. */
  --dropzone-line: #8d95a2;
  --brand: #2f54eb;
  --navy: #0e1217;
  --brand-soft: #eef2ff;
  --ok: #0a7d5a;
  --ok-soft: #e7f6ef;
  --warn: oklch(56% 0.13 70);
  --warn-soft: oklch(95% 0.05 80);
  --bad: oklch(50% 0.16 27);
  --bad-soft: oklch(95% 0.04 27);
  --busy: #475569;
  --busy-soft: #eef1f5;
  --r: 8px;
  --sans: system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", Consolas, "Courier New", monospace;
  /* Placeholder text inside the field grid. Lighter than --muted so an empty
     field never reads as a filled one, but no lighter: this placeholder says
     "enter manually", which is an instruction the operator has to act on, not
     decoration. Measured 4.61:1 on --row-tint and 4.78:1 on --panel — the first
     draft (#939bab) was 2.7:1 and failed on both grounds. */
  --faint: #6b7381;
  /* Alternating tint for the left column of the field grid. Deliberately COOL,
     mixed off --paper. The prototype this design comes from used a cream
     (#fbf2e1) here — but that exact colour is --warn-soft elsewhere in this app,
     the "this field was corrected" ground. Decorating half the grid with the
     warning colour would teach the operator that a warm cell means nothing,
     which is the one lesson a read-verification UI must never teach. */
  --row-tint: color-mix(in oklab, var(--paper) 55%, var(--panel));
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
  background:var(--paper); color:var(--ink);
  font:15px/1.5 system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  min-height:100dvh;
}
header{
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:18px 16px 20px; max-width:860px; margin:0 auto;
  /* keep the top bar anchored (brand left, language buttons right) in every
     language — without this, dir=rtl for Hebrew flips the row and the language
     buttons jump to the opposite side */
  direction:ltr;
}
.brand{display:flex; align-items:center; gap:12px; min-width:0}
.brand-mark{
  width:44px; height:44px; flex:0 0 auto; display:grid; place-items:center;
  border-radius:10px; background:var(--brand); color:#fff;
}
.brand-mark svg{width:24px; height:24px}
header h1{font-size:19px; line-height:1.1; margin:0; letter-spacing:-.01em}
header .brand p{font-size:12px; line-height:1.2; margin:3px 0 0; color:var(--muted); letter-spacing:.03em}
header .ver{display:block; margin-top:2px; font:10px var(--mono); color:var(--muted); opacity:.7}
header .spacer{flex:1 1 auto; min-width:8px}
button.langbtn{cursor:pointer; font-size:12px; border-radius:99px; padding:3px 12px}
/* The group renders the languages you can switch TO, so nothing said which one
   you were already in. A span, not a disabled button: it must not become a
   fourth tap target in a header that already wraps on a phone. */
.langbtn.is-current{
  display:inline-flex; align-items:center; font:700 11px/1 var(--mono);
  letter-spacing:.06em; padding:3px 2px; color:var(--muted);
  border:0; background:none;
}
header .langgroup{display:flex; gap:6px; flex:0 0 auto}
header .pill{
  display:inline-flex; align-items:center; gap:7px; min-height:34px;
  font-size:12px; font-weight:600; color:#3a414c; border:1px solid var(--line);
  border-radius:999px; padding:0 13px; background:var(--panel);
  max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
header .pill svg{width:15px; height:15px; color:var(--ok); flex:0 0 auto}
main{max-width:860px; margin:0 auto; padding:0 16px 40px}
.hidden{display:none !important}

@media (max-width: 520px){
  header{ padding:18px 14px 22px; gap:12px; }
  .brand{gap:12px}
  .brand-mark{width:52px; height:52px; border-radius:14px}
  .brand-mark svg{width:28px; height:28px}
  header h1{font-size:21px}
  header .brand p{font-size:13px}
  header .ver{ font-size:11px; }
  header .spacer{ display:none; }
  header .pill{ order:4; flex:1 1 100%; text-align:center; padding:6px 12px; }
}

/* ---- scan stage ---- */
.stage{
  position:relative; background:var(--panel); border:2px dashed var(--dropzone-line);
  border-radius:12px; min-height:300px; padding:32px 32px 62px;
  display:flex; align-items:center; justify-content:center; text-align:center;
  box-shadow:0 1px 3px rgba(16,18,23,.05);
}
.stage .idle{
  color:var(--ink); padding:0 8px 10px; max-width:540px;
}
.idle .icon{
  width:58px; height:58px; border-radius:15px; margin:0 auto 16px;
  background:var(--brand-soft); color:var(--brand); display:grid; place-items:center;
}
.idle .icon svg{width:26px; height:26px}
.idle strong{font-size:19px; line-height:1.2; display:block; margin-bottom:7px; letter-spacing:-.02em}
/* 14.5px, not 13: this line is the scanning instruction ("include the two code
   lines"), the one sentence that decides whether the read succeeds, and it is
   read by operators who are not all twenty-five */
.stage .idle p{margin:0 0 20px; color:var(--muted); font-size:14.5px}
.idle .btns{display:flex; gap:10px; justify-content:center; flex-wrap:wrap}
.idle .btns button{
  min-height:42px; display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:0 18px; border-radius:var(--r); font-weight:600; font-size:14px; letter-spacing:.003em;
}
.idle .btns button svg{width:17px; height:17px}
/* Upload and "Scan from Scanner" shared one rule, so on the office PC — the only
   place the scanner button exists — the two ranked equally and the page had no
   primary action. Upload is the action for every host. */
.idle .btns button.primary{min-height:50px; padding:0 22px; font-size:15px}
.idle .btns button.primary svg{width:19px; height:19px}
/* The offline promise, repeated where it is actually acted on. The header pill
   is seen on arrival and forgotten by the time a passport is dropped; this line
   sits under the button that commits one. Same string, same key.

   Selector carries `.stage .idle p` on purpose: this is a <p> inside the idle
   block, so the instruction rule above matches it too and wins on specificity —
   the note rendered at the instruction's own 14.5px and tied with the sentence
   it is supposed to sit under. */
.stage .idle p.idle-privacy{
  display:flex; align-items:center; justify-content:center; gap:6px;
  margin:14px 0 0; font-size:12px; color:var(--muted);
}
.idle-privacy svg{width:14px; height:14px; flex:0 0 auto}
/* Document picker: shown in the stage, in place of the idle prompt, when one
   scan holds more than one passport. */
.docpick{color:var(--ink); padding:0 8px 10px; max-width:640px}
.docpick strong{font-size:19px; line-height:1.2; display:block; margin-bottom:7px; letter-spacing:-.02em}
.docpick p{margin:0 0 20px; color:var(--muted); font-size:13px}
.docpick-grid{display:flex; gap:14px; justify-content:center; flex-wrap:wrap; margin-bottom:18px}
.docpick-tile{
  display:flex; flex-direction:column; align-items:center; gap:8px;
  padding:10px; border-radius:var(--r); background:var(--panel); cursor:pointer;
}
.docpick-tile canvas{
  display:block; max-width:190px; max-height:190px; width:auto; height:auto;
  border:1px solid var(--line); border-radius:6px;
}
.docpick-tile span{
  font-size:11px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--muted);
}
/* the tile itself declares no border — the bordered element is its canvas, so
   the old `.docpick-tile:hover{border-color}` painted nothing and the picker,
   the one place the operator chooses WHICH passport to read, gave no response */
.docpick-tile:hover canvas{border-color:var(--brand)}
.docpick-tile:hover span{color:var(--brand)}
/* already read once — still pickable, just not the obvious next choice */
.docpick-tile.done{opacity:.5}

/* ---- auto-read run ----------------------------------------------------
   One sheet, several passports, read in turn. The tiles are a picture of what
   is on the glass, so they carry the run: which passport is being read, which
   name came back from it, and which one needs another try.

   TILE ORDER IS SPATIAL, NOT TEXTUAL. showDocumentPicker sorts the boxes by
   their position on the sheet (top to bottom, then left to right), so in an RTL
   locale the row must NOT mirror — tile 1 would land on the right while its
   passport sits on the left of the page, and the strip would stop being a map
   of the glass. The captions inside each tile localize normally. */
.docpick-grid{direction:ltr}
.docpick-run{font-size:19px; line-height:1.2; display:block; margin-bottom:14px; letter-spacing:-.02em}
.runstrip{margin:0 0 14px; text-align:center}
.runstrip strong{
  display:block; margin-bottom:10px; font-size:12px; font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--muted);
}
.runstrip .docpick-grid{gap:10px; margin-bottom:0}
.runstrip .docpick-tile canvas{max-width:132px; max-height:132px}
/* The read name is the payload of the whole strip, so it is the one caption
   that gets sentence case and room to breathe rather than the uppercase
   micro-label the other states use. */
.docpick-tile.is-read span,.docpick-tile.is-attention span{
  text-transform:none; letter-spacing:0; font-size:12px; max-width:190px;
}
/* Green for a read the trust chain let through, amber for one it held something
   back on — the same two meanings the result verdict uses, so the strip and the
   card never tell different stories about the same passenger. */
.docpick-tile.is-read span{color:var(--ink)}
.docpick-tile.is-read canvas{border-color:var(--ok)}
.docpick-tile.is-attention span{color:var(--warn); font-weight:700}
.docpick-tile.is-attention canvas{border-color:var(--warn)}
.docpick-tile.is-failed{opacity:.62}
.docpick-tile.is-failed span{color:var(--bad)}
.docpick-tile.is-reading{position:relative}
.docpick-tile.is-reading span{color:var(--brand)}
.docpick-tile[disabled]{cursor:default}
/* The only motion in the run: an indeterminate sweep under the tile currently
   being read, so a 30-second sheet never looks stalled. */
.docpick-bar{
  position:absolute; inset-inline:10px; bottom:6px; height:2px;
  border-radius:2px; overflow:hidden; background:var(--line);
}
.docpick-bar::after{
  content:""; position:absolute; inset-block:0; inline-size:40%;
  background:var(--brand); border-radius:2px;
  animation:docpick-sweep 1.15s ease-in-out infinite;
}
@keyframes docpick-sweep{
  0%{transform:translateX(-110%)}
  100%{transform:translateX(260%)}
}
/* Hover and focus were instant everywhere — eleven states snapping between two
   values. These are short enough to read as responsiveness rather than
   animation, and they touch only colour, so nothing here can cause a reflow. */
button, .docpick-tile canvas, .docpick-tile span, .session-table tbody tr,
.smart-field-choice, .imgzoom-close, .session-table .rowdel{
  transition:background-color .12s ease, border-color .12s ease, color .12s ease;
}
@media (prefers-reduced-motion:reduce){
  .docpick-bar::after{animation:none; inline-size:100%; opacity:.55}
  /* the state change still has to happen — just without the tween */
  button, .docpick-tile canvas, .docpick-tile span, .session-table tbody tr,
  .smart-field-choice, .imgzoom-close, .session-table .rowdel{ transition:none }
}
.stage.dragover{border-color:var(--brand); background:var(--brand-soft)}
.statusline{
  display:grid; grid-template-columns:10px minmax(0, 1fr) max-content; gap:10px; align-items:center;
  font-size:12px; color:var(--muted); min-height:36px;
}
/* only the stage's copy is parked at the bottom of the drop zone; the result
   view's second mount point flows normally under the verdict */
.stage .statusline{ position:absolute; inset-inline:28px; bottom:22px; }
.result-statusline{ margin:0 0 12px; }
.statusline .dot{width:8px;height:8px;border-radius:50%;background:var(--line); justify-self:center}
.statusline .dot.on{background:var(--ok)}
.statusline .dot.busy{background:var(--busy)}
.statusline .dot.bad{background:var(--bad)}
.status-text{
  min-width:0; max-width:100%; width:fit-content; justify-self:start;
  /* the two most useful errors in the app ("export the first page as JPG/PNG",
     "flatten the page, avoid glare") are ~100 chars — ellipsising them to one
     line hid the only part that says what to do next */
  overflow-wrap:anywhere;
  border:1px solid transparent; border-radius:var(--r);
}
/* idle: plain muted text hugging the dot. busy/ready/failed: a soft,
   content-sized chip in the app's standard tinted style (matches .verdict). */
.statusline.is-busy .status-text,
.statusline.is-bad .status-text,
.statusline.is-on .status-text{ padding:5px 11px; font-weight:600; }
.statusline.is-busy .status-text{ background:var(--busy-soft); border-color:var(--line); color:var(--busy); }
.statusline.is-on .status-text{ background:var(--ok-soft); border-color:oklch(86% 0.05 152); color:var(--ok); }
/* a failed read used to wear the amber WORKING chip and never clear it, so the
   app sat looking busy forever and failure was indistinguishable from progress */
.statusline.is-bad .status-text{ background:var(--bad-soft); border-color:oklch(84% 0.06 27); color:var(--bad); }
#btnPhotoFolder{
  min-height:34px; max-width:min(230px, 34vw); display:inline-flex; align-items:center; gap:7px;
  border-color:#d3dad5; border-radius:8px; padding:0 11px;
  color:var(--muted); background:var(--panel); font-size:12px;
  justify-self:end; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:0 0 auto;
}
#btnPhotoFolder::before{
  content:""; width:14px; height:14px; flex:0 0 auto; background:currentColor;
  -webkit-mask:url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='black' d='M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2h6.5A2.5 2.5 0 0 1 21 8.5v1a1 1 0 1 1-2 0v-1a.5.5 0 0 0-.5-.5h-7.33l-2-2H5.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5H7a1 1 0 1 1 0 2H5.5A2.5 2.5 0 0 1 3 17.5v-11Zm5.5 5A2.5 2.5 0 0 1 11 9h8.39a2 2 0 0 1 1.88 2.68l-2.17 6A2 2 0 0 1 17.22 19H8.83a2 2 0 0 1-1.88-2.68l1.55-4.82ZM11 11a.5.5 0 0 0-.47.33L8.83 17h8.39l2.17-6H11Z'/%3E%3C/svg%3E") center/contain no-repeat;
  mask:url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='black' d='M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2h6.5A2.5 2.5 0 0 1 21 8.5v1a1 1 0 1 1-2 0v-1a.5.5 0 0 0-.5-.5h-7.33l-2-2H5.5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5H7a1 1 0 1 1 0 2H5.5A2.5 2.5 0 0 1 3 17.5v-11Zm5.5 5A2.5 2.5 0 0 1 11 9h8.39a2 2 0 0 1 1.88 2.68l-2.17 6A2 2 0 0 1 17.22 19H8.83a2 2 0 0 1-1.88-2.68l1.55-4.82ZM11 11a.5.5 0 0 0-.47.33L8.83 17h8.39l2.17-6H11Z'/%3E%3C/svg%3E") center/contain no-repeat;
}
#btnPhotoFolder.folder-set{color:var(--ok);border-color:#9ed5c1;background:var(--ok-soft)}

@media (max-width:700px){
  .stage{min-height:340px}
  /* Stack the idle/processing content and status line in normal column flow. */
  .stage{ flex-direction:column; justify-content:center; gap:18px; padding:28px 16px 22px; }
  .stage .idle{padding-bottom:0}
  .stage .idle p{font-size:14px}
  .idle .btns button{width:100%; max-width:240px}
  .statusline{grid-template-columns:10px minmax(0, 1fr)}
  .stage .statusline{ position:static; inset-inline:auto; bottom:auto; width:100%; min-height:0; }
  #btnPhotoFolder{grid-column:2; justify-self:start; max-width:100%}
}

/* ---- controls ---- */
button{
  font:inherit; border:1px solid var(--line); background:var(--panel); color:var(--ink);
  border-radius:var(--r); padding:9px 16px; cursor:pointer;
}
button:hover{border-color:var(--line-strong)}
button:focus-visible{outline:2px solid var(--brand); outline-offset:2px}
button.primary{background:var(--brand); border-color:var(--brand); color:#fff}
button.primary:hover{filter:brightness(.94); border-color:var(--brand)}
button:disabled{opacity:.5; cursor:default}
button.alt{background:var(--navy); border-color:var(--navy); color:#f2f4f7}
button.alt:hover{background:#222a38; border-color:#222a38}

/* ---- result ---- */
/* The verdict carries the per-field uncertain-character lines on non-verified
   scans (multi-line), so it must preserve line breaks. */
.verdict{
  display:flex; align-items:center; gap:10px; margin:18px 0 10px;
  padding:12px 16px; border-radius:var(--r); border:1px solid var(--line);
  font-weight:600; white-space:pre-wrap;
}
.verdict.ok{background:var(--ok-soft); border-color:oklch(80% 0.07 152); color:var(--ok)}
.verdict.warn{background:var(--warn-soft); border-color:oklch(82% 0.08 80); color:var(--warn)}
.verdict.bad{background:var(--bad-soft); border-color:oklch(84% 0.06 27); color:var(--bad)}
.expiry-alert{
  margin:0 0 12px; padding:11px 16px; border-radius:var(--r);
  border:1px solid oklch(82% 0.08 60); background:var(--warn-soft); color:oklch(42% 0.1 55);
  font-size:14px;
}
.expiry-alert.bad{border-color:oklch(80% 0.08 27); background:var(--bad-soft); color:var(--bad)}
/* The field grid reads as one document table, not ten cards: no gaps, hairline
   rules between cells, one border around the whole thing. Two fixed columns
   rather than the old `auto-fill minmax()` — auto-fill cannot be capped at two,
   and at a full-width 788px container it would silently become three, which
   breaks the nth-child(odd) column tint and rules below. */
.fields{
  display:grid; grid-template-columns:1fr 1fr; gap:0;
  margin-bottom:14px; border:1px solid var(--line); border-radius:var(--r);
  background:var(--panel);
}
.result-body{display:flex; gap:16px; align-items:flex-start}
.result-body .fields{flex:1; min-width:0}
.passport-preview{flex:0 0 300px; margin:0; position:sticky; top:12px}
.passport-preview img{
  width:100%; display:block; border:1px solid var(--line);
  border-radius:var(--r); cursor:zoom-in; background:var(--panel);
}
.passport-preview figcaption{font-size:12px; color:var(--muted); margin-top:6px; text-align:center}
/* Stack the preview above the fields at 820px, not 700px. The fields grid is a
   flex sibling of a fixed 300px preview, so between 701px and ~820px it was
   squeezed to ~353px — two columns of 176px, too narrow for a label plus a
   status pill. Stacking earlier gives the grid the full content width exactly
   where it would otherwise be starved, and the preview was cramped in that band
   anyway. A viewport media query is enough here BECAUSE this rule is what
   decides whether the preview is taking the space in the first place. */
@media (max-width:820px){
  .result-body{flex-direction:column}
  .passport-preview{flex:none; position:static; width:100%; max-width:440px; margin:0 auto}
  /* Once .result-body is a column, `flex:1` governs height and align-items
     :flex-start leaves the grid at its shrink-to-fit width — it stopped short of
     the right edge with the space just empty. Claim the full row explicitly. */
  .result-body .fields{width:100%}
}
/* One column when even the full content width cannot hold two readable cells. */
@media (max-width:560px){
  .fields{grid-template-columns:1fr}
}
/* Full-screen preview. The image gets a DEFINITE box and object-fit does the
   fitting — deliberately not `max-width/max-height:100%` on a centred grid
   item, which is what this used to be and what broke it: a percentage
   max-height resolves against the grid area, that area counted as indefinite,
   so max-height was silently dropped and only max-width applied. Every image
   was then scaled to the container WIDTH, and anything taller than the viewport
   was clipped with nothing to scroll to.

   It looked fine for years because a phone photo of a passport is landscape, so
   width-fitting happens to fit it. A scanned page is portrait A4 (1700x2200),
   so it was always cut down to its top band and blown up — measured at 314px
   tall inside a 219px box. Percentages here resolve against a fixed, inset:0
   container that is definite in BOTH axes, so the fit is exact at any aspect. */
.imgzoom{
  position:fixed; inset:0; z-index:100; background:oklch(18% 0.02 250 / .88);
  padding:24px; overflow:hidden; cursor:zoom-in;
}
.imgzoom img{
  width:100%; height:100%; object-fit:contain; border-radius:8px;
  transform-origin:0 0; will-change:transform;
  /* the browser must not claim the drag/pinch before the handler sees it */
  touch-action:none; user-select:none; -webkit-user-drag:none;
}
.imgzoom.is-zoomed{cursor:grab}
.imgzoom.is-panning{cursor:grabbing}
.imgzoom-close{
  /* logical, not `right` — this was the one control in the app that stayed
     top-right on the Hebrew page while everything around it mirrored */
  position:absolute; top:12px; inset-inline-end:12px; z-index:1; width:40px; height:40px;
  display:grid; place-items:center; border-radius:50%; cursor:pointer;
  background:oklch(100% 0 0 / .14); color:#fff; border:1px solid oklch(100% 0 0 / .22);
}
.imgzoom-close:hover{background:oklch(100% 0 0 / .24)}
.imgzoom-close svg{width:19px; height:19px}
.imgzoom-hint{
  position:absolute; left:0; right:0; bottom:14px; text-align:center; pointer-events:none;
  font-size:11px; letter-spacing:.06em; text-transform:uppercase;
  color:oklch(100% 0 0 / .62);
}
.field{
  background:var(--panel); padding:13px 16px 14px;
  border-bottom:1px solid var(--line);
}
/* the vertical rule between the two columns */
.field:nth-child(odd){border-inline-end:1px solid var(--line)}
/* A faint tint on the left column, so the eye tracks across a row instead of
   down a wall of identical cells. Cool, never warm — see --row-tint. */
.field:nth-child(odd){background:var(--row-tint)}
/* Corner cells round to meet the container's own radius. Logical properties, so
   the Hebrew (dir=rtl) grid rounds the corners it actually has. These assume an
   even, two-per-row item count — true of #fieldsGrid, which always renders the
   ten FIELD_DEFS entries; .extras-grid gets its own rules below. */
#fieldsGrid > .field:first-child{border-start-start-radius:var(--r)}
#fieldsGrid > .field:nth-child(2){border-start-end-radius:var(--r)}
#fieldsGrid > .field:nth-last-child(2){border-end-start-radius:var(--r)}
#fieldsGrid > .field:last-child{border-end-end-radius:var(--r)}
#fieldsGrid > .field:nth-last-child(-n+2){border-bottom:0}
@media (max-width:560px){
  .fields > .field{border-inline-end:0}
  #fieldsGrid > .field:nth-child(2){border-start-end-radius:0}
  #fieldsGrid > .field:first-child{border-start-end-radius:var(--r)}
  #fieldsGrid > .field:nth-last-child(2){border-end-start-radius:0; border-bottom:1px solid var(--line)}
  #fieldsGrid > .field:last-child{border-end-start-radius:var(--r)}
  /* one column: alternate by row, which nth-child(odd) already does */
}
.field-head{display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:5px; position:relative}
.field-head > label:first-child{display:flex; align-items:center; justify-content:flex-start; gap:7px; min-width:0; font:600 11px var(--sans); letter-spacing:.05em; text-transform:uppercase; color:var(--muted)}
/* the label text yields first — the status pill beside it must never be the
   thing that gets clipped out of a narrow cell */
.field-head > label:first-child > span:first-child{min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
/* One status pill per field. Red UNVERIFIED (a value no check digit will vouch
   for, including every withheld field) always wins over blue MANUAL (a field
   with no OCR source, so the operator has to type it). Deliberately NOT
   uppercased: the Russian string is "не проверено", which uppercased beside
   ГРАЖДАНСТВО does not fit a column cell. */
.field-tag{
  flex:0 0 auto; font-style:normal; font-weight:600; font-size:9.5px;
  letter-spacing:.04em; padding:1px 6px; border-radius:5px;
  border:1px solid currentColor; white-space:nowrap;
}
.field-tag.is-unverified{color:var(--bad); background:var(--bad-soft)}
.field-tag.is-manual{color:var(--brand); background:var(--brand-soft)}
.field .frow{display:flex; gap:8px; align-items:center}
/* Every value in this grid is monospaced, with no exceptions. An earlier pass
   set nationality/gender/issuer proportional on the theory that they are words —
   they are not, the app stores the raw MRZ codes (ISR, F), so it was switching
   fonts mid-grid for three cells with no visible reason. A fixed advance is also
   what makes a transposed digit easy to catch, which is the whole job here. */
.field .frow input{
  width:100%; min-width:0; flex:1; border:0; background:transparent; color:var(--ink);
  font:600 16px var(--mono); padding:2px 0; direction:ltr; text-align:left;
  border-bottom:1.5px solid transparent; transition:border-color .15s;
}
.field .frow input::placeholder{font:400 13px var(--sans); color:var(--faint); letter-spacing:0}
.field .frow input:focus{outline:none; border-bottom-color:var(--brand)}
/* Every extracted passport field is corrected here, so this is the app's main
   editing surface — and keyboard focus on it was a 1px underline, well under the
   2px / 3:1 minimum a focus indicator has to meet. Keyboard focus now gets the
   same ring as every button; the mouse keeps the quieter underline above. */
.field .frow input:focus-visible{outline:2px solid var(--brand); outline-offset:3px; border-radius:3px}
.field.lowconf .frow input{color:var(--warn)}
.field.bad .frow input{color:var(--bad)}
/* value auto-filled by the background VIZ reader — marked until user touches it.
   1.5px to match the resting underline, so the marking appears and disappears
   without the baseline shifting */
.field .frow input.autofilled{border-bottom:1.5px dotted var(--brand); color:var(--brand)}
.smart-field-choice{
  position:absolute; inset-inline-end:0; top:50%; transform:translateY(-50%);
  width:20px; height:20px; display:grid; place-items:center; cursor:pointer;
  border:1px solid var(--line); border-radius:5px; background:var(--panel);
}
.smart-field-choice:hover{border-color:var(--brand); background:var(--brand-soft)}
.smart-field-choice:focus-within{outline:2px solid var(--brand); outline-offset:1px}
.smart-field-choice input{width:14px; height:14px; margin:0; accent-color:var(--brand); cursor:pointer}
.smart-field-choice input:focus-visible{outline:none}
body.smart-export-selecting .field-head{padding-inline-end:28px}
/* The per-field checkboxes only exist while choosing. The "no fields selected"
   warning is the exception: it shares this attribute but must stay on screen
   after choose mode closes, or leaving it with nothing ticked gives four dead
   export buttons and no reason anywhere on the page. */
body:not(.smart-export-selecting) [data-smart-choice]:not(.is-empty){display:none}
/* office-only extras (insurance): collapsed by default, dot = has values */
.office-extras{margin:0 0 14px}
.office-extras summary{
  display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none;
  font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--muted);
  padding:8px 2px; border-top:1px solid var(--line); list-style-position:inside;
}
.office-extras summary:hover{color:var(--ink)}
.office-extras .extras-grid{margin:10px 0 0}
/* Three cells in a two-column grid leaves an orphan, so the phone field takes
   the whole bottom row and both bottom corners. Written out rather than reusing
   #fieldsGrid's corner rules, which assume an even item count. */
.extras-grid > .field:first-child{border-start-start-radius:var(--r)}
.extras-grid > .field:nth-child(2){border-start-end-radius:var(--r)}
.extras-grid > .field:last-child{
  grid-column:1 / -1; border-inline-end:0; border-bottom:0;
  border-end-start-radius:var(--r); border-end-end-radius:var(--r);
}
@media (max-width:560px){
  .extras-grid > .field:nth-child(2){border-start-end-radius:0}
  .extras-grid > .field:first-child{border-start-end-radius:var(--r)}
}
.office-extras .extras-dot{color:var(--brand); font-size:9px; line-height:1}
/* Hebrew inputs: the base .field input forces LTR/mono — restore RTL + a font
   that renders Hebrew well. Hebrew names are not document data, so they take the
   proportional face rather than the monospace every MRZ value uses. */
.office-extras input[dir="rtl"]{direction:rtl; text-align:right; font:600 16px var(--sans)}
/* OCR name suggestion: one-click accept, clearly marked unverified */
.heb-suggest{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-top:10px; padding:8px 12px; font-size:12px; color:var(--muted);
  background:var(--panel); border:1px dashed var(--line); border-radius:var(--r);
}
.heb-suggest b{color:var(--ink); font-size:14px}
.heb-suggest button{
  min-height:26px; padding:2px 12px; cursor:pointer; font-size:12px; font-weight:600;
  border:1px solid var(--brand); border-radius:7px; background:var(--brand); color:#fff;
}
.heb-suggest .heb-suggest-note{font-size:10px}
.cbtn{
  border:1px solid var(--line-strong); background:var(--panel); border-radius:9px;
  width:34px; height:34px; min-width:34px; display:grid; place-items:center; padding:0; cursor:pointer;
  color:var(--muted);
  transition:color .15s, border-color .15s, background .15s, transform .12s;
}
.cbtn:hover{border-color:var(--brand); color:var(--brand); background:var(--brand-soft)}
.cbtn:active{transform:translateY(1px)}
.cbtn svg{width:16px; height:16px}
.cbtn.done{color:var(--ok); border-color:var(--ok)}
.amadeus{
  border:1px solid var(--line-strong); background:var(--panel); border-radius:12px;
  padding:14px 16px; margin-bottom:16px;
}
.amadeus .amhead{display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; gap:10px; flex-wrap:wrap}
.amadeus h2{margin:0; font-size:14px}
.amadeus .airline{display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); letter-spacing:.5px}
.amadeus .airline input{
  width:58px; text-transform:uppercase; font:700 14px var(--mono);
  border:1px solid var(--line); border-radius:8px; padding:6px 8px; text-align:center;
  background:var(--paper); color:var(--ink);
}
.amadeus .airline input:focus{outline:2px solid var(--brand); outline-offset:1px; border-color:var(--brand)}
.amadeus .amctl{display:flex; align-items:center; gap:12px; flex-wrap:wrap}
.amadeus .gds-select{
  font-size:13px; font-weight:600; padding:6px 8px; border:1px solid var(--line);
  border-radius:8px; background:var(--paper); color:var(--ink); cursor:pointer;
}
.cmdrow{display:flex; gap:8px; margin-bottom:8px}
.cmdrow input{
  flex:1; min-width:0; font:600 13px var(--mono); border:1px solid var(--line); border-radius:8px;
  padding:9px 12px; background:var(--paper); color:var(--ink);
  /* plaintext lets the typed Latin GDS command resolve LTR while the Hebrew
     placeholder resolves RTL — without this the LTR-forced input scrambled the
     Hebrew hint (the "SR DOCS" token jumped to the wrong end) */
  unicode-bidi:plaintext; text-align:left;
}
.amactions{display:flex; align-items:center; gap:12px; margin-top:4px; flex-wrap:wrap}
.amactions .hint{font-size:12px; color:var(--muted)}
.actions{display:flex; gap:10px; flex-wrap:wrap; margin-bottom:8px}
.export-panel{
  border:1px solid var(--line); background:var(--panel); border-radius:12px;
  padding:8px 10px; margin:0 0 8px;
}
.export-head{display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px}
.export-head h2{margin:0; font-size:13px}
.export-actions{display:flex; align-items:center; gap:6px; flex-wrap:wrap}
.export-actions button{padding:7px 10px; border-radius:7px; font-size:12px}
.smart-export-control{display:flex; align-items:center; gap:8px; margin-inline-start:auto}
#btnSmartChoose{flex:0 0 144px; inline-size:144px; white-space:nowrap}
.smart-selected-count{font-size:11px; color:var(--muted)}
.smart-selected-count.is-empty{color:var(--bad); font-weight:700}
.smart-selection-tools{display:flex; align-items:center; gap:4px; flex-wrap:nowrap}
.smart-selection-tools button{padding:5px 9px; border-radius:7px; font-size:11px}
.smart-selection-tools.hidden{display:none}
#btnExcelRows:disabled,#btnXlsx:disabled,
#btnSessionExcelRows:disabled,#btnSessionXlsx:disabled{opacity:.5; cursor:not-allowed}
.copylog{font-size:12px; color:var(--ok); min-height:18px; margin-bottom:10px}
.session-panel{
  border:1px solid var(--line); background:var(--panel); border-radius:12px;
  padding:12px 14px; margin:0 0 16px;
}
.session-head{display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px}
.session-head-info{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.session-head h2{margin:0; font-size:14px}
.session-count{font-size:12px; color:var(--muted)}
.session-clear{
  flex:0 0 auto; padding:6px 11px; border-radius:7px; font-size:12px; color:var(--muted);
}
.session-clear:hover{color:var(--bad); border-color:oklch(84% 0.06 27)}
.session-actions{display:flex; justify-content:flex-end; gap:6px; flex-wrap:wrap; margin:-4px 0 10px}
.session-actions button{padding:6px 10px; border-radius:7px; font-size:11px}
/* Six columns of passenger data that must not wrap (a passport number broken
   across two lines is worse than useless) do not fit a phone: measured at 417px
   inside a 314px panel, which pushed the whole PAGE 73px sideways rather than
   clipping. Scroll the table on its own axis and leave the page alone. */
#sessionList{overflow-x:auto; -webkit-overflow-scrolling:touch}
.session-table{font-size:12px}
.session-table td,.session-table th{white-space:nowrap}
.session-table td:first-child,.session-table th:first-child{width:42px; text-align:center}
.session-table .status-ok{color:var(--ok); font-weight:700}
.session-table .status-warn{color:var(--warn); font-weight:700}
.session-table tbody tr{cursor:pointer}
.session-table tbody tr:hover{background:color-mix(in srgb, var(--brand) 10%, transparent)}
.session-table tbody tr:focus-visible{outline:2px solid var(--brand); outline-offset:-2px}
.session-table tbody tr:hover td:first-child{font-weight:700}
/* per-row remove ✕ — muted until hover, never stretches its column */
.session-table td:last-child,.session-table th:last-child{width:28px; text-align:center}
.session-table .rowdel{
  border:0; background:transparent; cursor:pointer; padding:2px 6px; line-height:1;
  font-size:13px; color:var(--muted); border-radius:6px;
}
.session-table .rowdel:hover{color:var(--bad); background:color-mix(in srgb, var(--bad) 12%, transparent)}

table{width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--line); border-radius:var(--r); overflow:hidden; font-size:13px}
th,td{padding:7px 10px; border-bottom:1px solid var(--line); text-align:start}
/* mono cells (passport, expiry) hold LTR digits/Latin: direction:ltr keeps
   "15 SEP 2027" from scrambling under RTL bidi, and the alignment must be set
   per page direction EXPLICITLY — text-align:start can't be used because with
   any content-resolved bidi the digits resolve LTR and "start" lands left
   even on the Hebrew page, leaving the value off under its right-aligned
   header */
td.mono{font-family:var(--mono); direction:ltr; text-align:left}
[dir="rtl"] td.mono{text-align:right}
tr:last-child td{border-bottom:0}
th{background:var(--paper); font-weight:600; font-size:12px; color:var(--muted)}
.disclaimer{
  max-width:860px; margin:20px auto 0; padding:12px 18px 0;
  border-top:1px solid var(--line);
  /* 15px. This is the sentence the product promise rests on — the name carries
     no check digit, so a person has to read it off the document — and at 12px
     it was fine print. It is one node with two homes (see placeDisclaimer),
     so this size covers the scan screen and the result screen alike. */
  font-size:15px; line-height:1.5; color:var(--muted); overflow-wrap:anywhere;
  text-align:center;
}
/* On the result screen the notice sits under the verdict, inside the reading
   flow, so it does not need the foot-of-page separation the scan screen gives it. */
#disclaimerMountResult .disclaimer{ margin:0 0 14px; padding-top:10px; }
footer{
  max-width:860px; margin:24px auto 0; padding:0 18px 24px;
  /* #94a0af measured 2.76:1 on --paper — under AA, and this is where the "runs
     entirely in your browser" promise is written. --muted is the documented
     5.09:1. */
  font-size:13px; line-height:1.5; color:var(--muted); overflow-wrap:anywhere;
  text-align:center; white-space:pre-line;
}
footer .footer-legal{
  display:flex; gap:16px; justify-content:center; flex-wrap:wrap; margin:0 0 8px;
  /* footer is pre-line, for the newline inside the translated footer string.
     The link row must not inherit it. */
  white-space:normal;
}
/* There is no global a{} rule in this file, so an unstyled link here would render
   in UA default #0000EE. --brand is 5.41:1 on --paper, AA at 13px, and is already
   the app's link colour in the consent gate. Underlined as well as coloured:
   against --muted footer text, brand-vs-text is 1.06:1, which is not a
   distinction under WCAG 1.4.1. No direction pin — unlike the language chips
   these are translated words, and should follow dir=rtl. */
footer .footer-legal a{ color:var(--brand); text-decoration:underline }
@media (max-width: 520px){
  footer{ text-align:center; }
  .session-head{align-items:flex-start; flex-direction:column}
  .session-actions{justify-content:flex-start}
}

/* Touch targets: the compact buttons are deliberate on desktop, where the
   pointer is precise. On a phone the guidance is 44px, and the old rule both
   stopped at 40 and covered only four selectors — missing every one of the
   SMALLEST controls (the per-field export checkbox at 20px, the row ✕ at ~17,
   the language chips, the copy buttons, the "Use" on a Hebrew suggestion). */
@media (max-width:700px){
  .export-actions button, .session-actions button, .session-clear,
  .smart-selection-tools button, button.langbtn, .heb-suggest button,
  .office-extras summary{ min-height:44px; }
  /* The rewrite that added the small controls above missed the BIG ones. The
     four result-screen actions inherit button{padding:9px 16px} + 15px/1.5 and
     land at 43px -- short by a single pixel, which is why nobody spotted it. */
  .actions button{ min-height:44px; }
  /* Terms and Privacy were 20px tall, in the footer and inside the first-run
     gate -- the two links a person has to be able to open BEFORE accepting a
     binding agreement. inline-flex, not a bare min-height: these are inline
     boxes, where min-height has no effect until the box generates a block-level
     layout. The text stays where it was; only the hit area grows. */
  #consentGate .consent-links a, .footer-legal a{
    display:inline-flex; align-items:center; min-height:44px;
  }
  .cbtn{ width:44px; height:44px; }
  /* the checkbox and the ✕ stay visually small — the TAP area grows around them,
     which is what actually matters, and the table layout is unaffected */
  .smart-field-choice{ width:44px; height:44px; background:transparent; border-color:transparent }
  .smart-field-choice:hover{ background:var(--brand-soft) }
  /* the row ✕ deletes a passenger, so it is the last control that should be a
     near-miss tap; the table scrolls on its own axis, so the wider column is free */
  .session-table td:last-child, .session-table th:last-child{ width:44px }
  .session-table .rowdel{ width:44px; height:44px; padding:0 }
}

/* ---- first-run acceptance gate ---- */
#consentGate{
  max-width:min(460px, calc(100vw - 32px)); padding:26px 26px 22px;
  border:1px solid var(--line); border-radius:12px;
  background:var(--panel); color:var(--ink);
  box-shadow:0 18px 48px rgba(16,18,23,.18);
  /* the dialog is a direct child of <body>, which never carries dir — without
     this it stays LTR while the app behind it is Hebrew or Arabic */
  text-align:start;
}
#consentGate::backdrop{ background:rgba(14,18,23,.55) }
#consentGate .consent-langs{
  display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end;
  margin-bottom:14px; direction:ltr;
}
#consentGate h2{ margin:0 0 10px; font-size:19px; letter-spacing:-.01em }
#consentGate p{ margin:0 0 14px; font-size:14px; line-height:1.55; color:var(--ink) }
#consentGate .consent-links{ display:flex; gap:16px; flex-wrap:wrap; margin-bottom:20px }
#consentGate .consent-links a{ font-size:13px; color:var(--brand) }
#consentGate #consentAccept{
  min-height:44px; width:100%; font-weight:600; font-size:15px; border-radius:var(--r);
}

/* One source tree serves two deliberately separate products. The full build is
   the default; only build:demo flips the edition marker in generated output. */
[data-demo-only]{display:none!important}
html[data-scanpass-edition="demo"] [data-full-only]{display:none!important}
html[data-scanpass-edition="demo"] .demo-badge{
  display:inline-flex!important; align-items:center; min-height:28px; padding:4px 10px;
  border:1px solid color-mix(in srgb, var(--brand) 26%, var(--line)); border-radius:999px;
  background:var(--brand-soft); color:var(--brand); font-size:12px; font-weight:700;
}
html[data-scanpass-edition="demo"] #demoLimitView:not(.hidden){display:flex!important}
.demo-limit-view{max-width:860px; min-height:55vh; margin:0 auto; padding:48px 18px; align-items:center; justify-content:center}
.demo-limit-card{
  width:min(560px, 100%); padding:34px 30px; border:1px solid var(--line); border-radius:16px;
  background:var(--panel); box-shadow:0 14px 38px rgba(16,18,23,.08); text-align:center;
}
.demo-limit-kicker{display:block; margin-bottom:10px; color:var(--brand); font-size:12px; font-weight:800; letter-spacing:.08em; text-transform:uppercase}
.demo-limit-card h2{margin:0 0 12px; font-size:clamp(23px, 4vw, 32px); line-height:1.2}
.demo-limit-card p{margin:0; color:var(--muted); font-size:16px; line-height:1.6}
/* The demo's contact route: a quiet control in the header for the whole visit,
   and the card's one primary action once the five results are used.

   Both are buttons that COPY the address, not mailto: links -- a mailto: opens
   whatever mail app the machine defaults to, which on an office PC is an Outlook
   the operator never reads. Hence the button resets in both rules. */
html[data-scanpass-edition="demo"] .demo-contact{
  display:inline-flex!important; align-items:center; min-height:28px;
  color:var(--brand); font-size:13px; font-weight:700; text-decoration:underline; text-underline-offset:3px;
  border:0; padding:0; background:none; cursor:pointer; font-family:inherit;
}
html[data-scanpass-edition="demo"] .demo-contact-cta{
  display:inline-flex!important; align-items:center; justify-content:center; min-height:48px; margin-top:22px;
  padding:0 22px; border-radius:var(--r); background:var(--brand); color:#fff;
  font-size:15px; font-weight:700; text-decoration:none;
  border:0; cursor:pointer; font-family:inherit;
}
.demo-contact-cta:hover{filter:brightness(.94)}
.demo-contact:focus-visible, .demo-contact-cta:focus-visible{outline:2px solid var(--brand); outline-offset:2px}
html[data-scanpass-edition="demo"] .demo-limit-card .demo-contact-hint{display:block!important; margin-top:14px; font-size:14px}
.demo-contact-address{font-family:var(--mono); color:var(--ink); user-select:all}
