/* SharpOCR signed-in app — CSS translation of ui_kits/app/*.jsx from the
   design system (AppShell, SidebarNav, FileRow, Tabs, UsageMeter).

   Loaded by the dashboard, which Caddy proxies at /app while everything else
   falls through to this static site — so the two surfaces share tokens.css,
   icons.css and components.css and cannot drift apart.

   Layout per the kit: fixed 248px sidebar, sticky 60px topbar, scrolling
   content on --surface-page with 28px padding. */

html,body{height:100%}
body.app-body{background:var(--surface-page);overflow:hidden}

.app{display:flex;height:100vh;background:var(--surface-page)}

/* ---- sidebar ---- */
.sidebar{display:flex;flex-direction:column;width:var(--sidebar-w);flex:0 0 auto;
  background:var(--white);border-right:1px solid var(--border-subtle);
  padding:0 12px 12px;overflow-y:auto}
.sidebar-brand{display:flex;align-items:center;gap:8px;height:var(--topbar-h);
  padding:0 4px}
/* Tight, because it sits beside a 20px wordmark and a full-height badge next to
   a name reads as part of the name. */
.sidebar-brand .badge{height:18px;padding:0 6px;font-size:var(--text-2xs)}
.sidebar-nav{display:flex;flex-direction:column;gap:2px;margin-top:6px}
.sidebar-nav a{display:flex;align-items:center;gap:10px;height:34px;padding:0 8px;
  border:0;border-radius:var(--radius-sm);background:transparent;color:var(--text-body);
  font-family:var(--font-sans);font-size:var(--text-sm);font-weight:var(--weight-regular);
  cursor:pointer;text-align:left;transition:var(--transition-control);text-decoration:none}
.sidebar-nav a:hover{background:var(--ink-50);color:var(--text-body)}
.sidebar-nav a[aria-current="page"]{background:var(--indigo-50);color:var(--indigo-600);
  font-weight:var(--weight-medium)}
.sidebar-nav a .count{font-family:var(--font-mono);font-size:var(--text-2xs);
  color:var(--text-subtle);margin-left:auto}
.sidebar-section{margin:18px 8px 6px;font-size:var(--text-2xs);
  letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--text-subtle)}
.sidebar-foot{margin-top:auto}

/* The plan, above the meter. Bold and in the ink colour because it is the
   answer to "what am I paying for", and the numbers under it only mean
   something once you know it. */
/* A spent meter, monthly or daily. Colour rather than a separate element so
   the bar itself carries the state. */
.usage-fill.is-full{background:var(--red-600)}
.usage-plan{margin:0 0 8px;padding:0;font-family:var(--font-display);
  font-size:var(--text-xs);font-weight:var(--weight-bold);
  color:var(--text-strong);letter-spacing:var(--tracking-wide);
  text-transform:uppercase}
.usage{padding:12px;background:var(--ink-50);border-radius:var(--radius-md)}
.usage-head{display:flex;justify-content:space-between;font-size:var(--text-2xs);
  color:var(--text-muted);margin-bottom:7px}
.usage-head .n{font-family:var(--font-mono);color:var(--text-body)}
.usage-track{height:4px;background:var(--ink-200);border-radius:var(--radius-pill)}
.usage-fill{height:100%;background:var(--indigo-500);border-radius:var(--radius-pill)}

/* ---- topbar + content ---- */
.app-col{flex:1;display:flex;flex-direction:column;min-width:0}
.topbar{display:flex;align-items:center;gap:16px;height:var(--topbar-h);padding:0 28px;
  background:var(--white);border-bottom:1px solid var(--border-subtle);flex:0 0 auto}
.topbar-title{flex:1;min-width:0}
.topbar-title h1{font-family:var(--font-display);font-size:var(--text-md);
  font-weight:var(--weight-semibold);color:var(--text-strong);margin:0;
  letter-spacing:var(--tracking-snug)}
.topbar-title p{font-family:var(--font-mono);font-size:var(--text-2xs);
  color:var(--text-subtle);margin:1px 0 0}
.topbar-user{display:flex;align-items:center;gap:8px;padding-left:12px;
  border-left:1px solid var(--border-subtle)}
.avatar{display:flex;align-items:center;justify-content:center;width:28px;height:28px;
  border-radius:var(--radius-pill);background:var(--indigo-500);color:#fff;
  font-size:var(--text-xs);font-weight:var(--weight-semibold)}
.app-main{flex:1;overflow:auto;padding:28px}
.app-grid{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:24px;
  max-width:var(--container-xl)}
.app-stack{display:flex;flex-direction:column;gap:20px;min-width:0}

/* ---- FileRow ---- */
.filelist{background:var(--surface-card);border:1px solid var(--border-subtle);
  border-radius:var(--radius-card);box-shadow:var(--shadow-sm);overflow:hidden}
.filelist-head{display:flex;align-items:center;gap:10px;padding:14px 16px;
  border-bottom:1px solid var(--border-subtle)}
.filelist-head h3{font-family:var(--font-display);font-size:var(--text-base);
  font-weight:var(--weight-semibold);margin:0;flex:1}
.filerow{display:flex;align-items:center;gap:14px;padding:12px 16px;
  background:var(--white);border-bottom:1px solid var(--border-subtle);
  transition:background-color var(--dur-fast) var(--ease-standard);
  text-decoration:none;color:inherit}
.filerow:last-child{border-bottom:0}
a.filerow:hover{background:var(--ink-50)}
.filerow-glyph{display:flex;align-items:center;justify-content:center;width:32px;height:32px;
  flex:0 0 auto;border-radius:var(--radius-sm);background:var(--surface-sunken);
  color:var(--text-muted)}
.filerow-body{flex:1;min-width:0}
.filerow-name{font-size:var(--text-sm);font-weight:var(--weight-medium);
  color:var(--text-strong);margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.filerow-meta{font-size:var(--text-xs);color:var(--text-subtle);margin:2px 0 0;
  font-family:var(--font-mono)}
.filerow-conf{font-family:var(--font-mono);font-size:var(--text-xs)}
.conf-high{color:var(--teal-600)}.conf-mid{color:var(--amber-600)}.conf-low{color:var(--red-600)}
.filerow-actions{display:flex;gap:2px;opacity:.45;
  transition:opacity var(--dur-fast) var(--ease-standard)}
.filerow:hover .filerow-actions{opacity:1}
/* A row that carries its own controls. The link shrinks to the body so the
   buttons beside it are not inside it — a <form> nested in an <a> is invalid
   and browsers recover by relocating it. The hover tint moves to the row for
   the same reason: the anchor is no longer the row. */
/* border-bottom:0 is load-bearing, not tidying. tokens.css gives every <a> a
   1px indigo underline, which is right for a link inside prose and wrong for a
   link that *is* a table row: .filerow-link is a flex row spanning the whole
   entry, so the underline drew a dark line under every document in the list.
   The plain `a.filerow` used elsewhere never showed it because .filerow sets a
   border-bottom of its own and outranks the element selector; this one sets
   none, so the token rule won. */
.filerow-link{display:flex;align-items:center;gap:14px;flex:1;min-width:0;
  text-decoration:none;color:inherit;border-bottom:0}
.filerow-linked:hover{background:var(--ink-50)}
/* Keyboard users get no hover, so the actions have to appear on focus too —
   otherwise the delete button is reachable by Tab and invisible while focused. */
.filerow:focus-within .filerow-actions{opacity:1}

.iconbtn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
  border-radius:var(--radius-sm);background:transparent;border:1px solid transparent;
  color:var(--text-muted);cursor:pointer;transition:var(--transition-control)}
.iconbtn:hover{background:var(--action-ghost-hover);color:var(--indigo-500)}

/* ---- Tabs ---- */
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border-subtle)}
.tab{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 12px;
  background:none;border:0;border-bottom:2px solid transparent;margin-bottom:-1px;
  font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-muted);
  cursor:pointer;transition:var(--transition-control);text-decoration:none}
.tab[aria-selected="true"]{border-bottom-color:var(--action-primary);
  color:var(--text-strong);font-weight:var(--weight-medium)}

/* ---- page rail (ResultScreen) ---- */
/* Document beside text. One column until there is a document to show, so a job
   whose file has been deleted does not render half an empty grid. minmax(0,1fr)
   twice: without the 0 floor an <object> with a wide PDF pushes the column past
   its share and the text pane is squeezed to nothing. */
.doc-split{display:grid;grid-template-columns:minmax(0,1fr);gap:16px;min-width:0}
.doc-split.has-doc{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.doc-pane{min-width:0;display:flex;flex-direction:column}
/* A viewport tall enough to read a page in, not the intrinsic height of the
   PDF — <object> has none, so without this it collapses to a few pixels. */
/* Tab panes driven by the radio that precedes them, so switching needs no
   JavaScript and no CSP nonce. The two inputs are visually hidden (.sr-only)
   and remain keyboard-reachable, which is what keeps the labels operable
   without a script. */
.tabpick{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.tabpane{display:none}
/* Direct sibling combinator from the radio to the pane. Both radios sit ahead
   of the toolbar as children of .doc-pane so this can reach; see the note in
   document.html. When there is no JSON tab only the text radio is rendered,
   checked, and the first rule alone does the job — no :has() needed. */
#tab-text:checked~.tabpane-text{display:block}
#tab-json:checked~.tabpane-json{display:block}
#tab-md:checked~.tabpane-md{display:block}
/* The checked tab has to look chosen. .tab already carries the styling for a
   selected tab via aria-selected; labels get it from the radio instead. */
#tab-text:checked~.doc-toolbar .tab[for=tab-text],
#tab-json:checked~.doc-toolbar .tab[for=tab-json],
#tab-md:checked~.doc-toolbar .tab[for=tab-md]{color:var(--text-strong);
  box-shadow:inset 0 -2px 0 var(--indigo-500)}
/* One copy button at a time — the one for the pane on screen. Driven by the
   same radio as the panes themselves, so a tab can never show another tab's
   copy button. Hidden by default rather than shown, so a pane added without a
   matching rule fails visibly (no button) instead of silently offering the
   wrong one. */
.copy-for{display:none}
#tab-text:checked~.doc-toolbar .copy-for-text,
#tab-json:checked~.doc-toolbar .copy-for-json,
#tab-md:checked~.doc-toolbar .copy-for-md{display:inline-flex}
/* A label that reads as a link inside prose — the Markdown tab's note points
   back at the Text tab, and the control that gets you there is a <label>. */
.linky{color:var(--text-link);cursor:pointer;
  border-bottom:1px solid rgba(43,58,143,.28)}
.linky:hover{color:var(--text-link-hover)}
.tabpick:focus-visible~.doc-toolbar .tab[for]{outline:2px solid var(--border-brand);
  outline-offset:2px}
.tab[for]{cursor:pointer}
.doc-toolbar{display:flex;justify-content:space-between;align-items:center;
  gap:8px;flex-wrap:wrap}
.doc-toolbar .btn{white-space:nowrap}
/* The tab strip has to be able to wrap, not just the toolbar around it. In a
   two-column split the pane is ~264px and three tabs plus the AI marker measure
   293 — the strip overflowed its own pane rather than wrapping, because only
   its parent was allowed to. */
.doc-toolbar [role=tablist]{flex-wrap:wrap;min-width:0}
/* The marker on a rewritten Markdown tab. Tight on purpose: it sits inside a
   tab that already has a label, and a full-size badge is what pushed the strip
   past the pane in the first place. */
.tab .badge{padding:0 5px;font-size:var(--text-2xs);line-height:16px}
/* The rendered document. No longer an <iframe> with a height of its own — it is
   a scroll container of canvases now, so the height belongs to the pane and both
   panes take it from the same rule. That is what keeps the two halves of the
   split exactly aligned; when the frame owned its height, the text pane matched
   it only by coincidence. */
.doc-frame{width:100%;border:0;background:var(--ink-50);
  border-bottom-left-radius:var(--radius-card);
  border-bottom-right-radius:var(--radius-card);
  padding:16px;display:flex;flex-direction:column;gap:16px}
/* One page. The aspect ratio is set per page by pdfview.js from the PDF's own
   media box, before anything is drawn, so the pane has its final scroll height
   from the first frame and the rail's measurements do not move under it. */
.pdf-page{position:relative;background:var(--white);border-radius:var(--radius-sm);
  box-shadow:var(--shadow-sm);overflow:hidden;flex:0 0 auto}
.pdf-page canvas{display:block;width:100%;height:auto}
/* A page whose render failed. Named rather than left blank: a white rectangle
   in a stack of pages reads as a blank page in the document. */
.pdf-page.is-error{background:var(--ink-100);
  background-image:repeating-linear-gradient(45deg,transparent 0 8px,
    rgba(16,19,35,.05) 8px 16px)}
.doc-split.has-doc .doc-pane{height:min(72vh,780px)}
@media (max-width:1100px){
  /* Side by side stops being a comparison and starts being two slivers. */
  .doc-split.has-doc{grid-template-columns:minmax(0,1fr)}
  .doc-split.has-doc .doc-pane{height:min(56vh,560px)}
}
/* ------------------------------ the staged upload list ------------------ */
/* The Process button, which lives in the staged list's header rather than in
   the drop box — picking files and processing them are two different acts and
   were two labels on one control.

   The animation is deliberately almost nothing: a slow breath on the shadow,
   no movement, no colour change. It exists because the button appears in a
   part of the page the eye has already passed over, and something has to say
   "the next step is here" without behaving like an advertisement. It runs only
   while the button is live, so a disabled or mid-upload button is still. */
@keyframes stage-breathe{
  0%,100%{box-shadow:0 0 0 0 rgba(43,58,143,.30)}
  50%{box-shadow:0 0 0 6px rgba(43,58,143,0)}
}
[data-stage-submit]:not(:disabled){animation:stage-breathe 2.8s var(--ease-standard) infinite}
[data-stage-submit]:hover,[data-stage-submit]:focus-visible{animation:none}
@media (prefers-reduced-motion: reduce){
  [data-stage-submit]:not(:disabled){animation:none}
}
/* Mid-upload. The label already says what is happening; this is for the glance
   that does not read it. */
@keyframes stage-spin{to{transform:rotate(360deg)}}
[data-stage-submit][data-busy] .icon{animation:stage-spin .9s linear infinite}
@media (prefers-reduced-motion: reduce){
  [data-stage-submit][data-busy] .icon{animation:none}
}
/* The header is a flex row and the button was the item that gave way: at
   "Process 2 documents" it was squeezed to 99px against a 138px label and
   rendered as "Process", which reads as the wrong button rather than a clipped
   one. The button keeps its width and the hint beside it is what shrinks. */
.filelist-head .btn{margin-left:auto;flex:0 0 auto}
.filelist-head .fine{min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
@media (max-width:720px){
  /* Below this the hint and the button cannot share a line at all. The hint is
     the half that goes: the button is the control. */
  .filelist-head .fine{display:none}
}
/* A row that has not been sent yet. Distinct from .filerow's other use — a
   parsed document — because these are still the customer's to change: the mode
   select and the remove button are the two things a queued row does not have. */
.stage-row{gap:12px}
.stage-mode{width:auto;min-width:180px;flex:0 0 auto;height:var(--control-h-sm,32px);
  font-size:var(--text-xs);padding:0 8px}
.stage-status{margin-left:8px}
/* A finished row's name becomes the way into its document. The Recent list
   below is server-rendered and does not know about this batch until a reload,
   so without this the customer has just watched five documents finish and has
   nowhere to click. */
.stage-open{color:var(--text-link);border-bottom:0}
.stage-open:hover{color:var(--text-link-hover);text-decoration:underline}
.stage-status:empty{display:none}
/* Four states, and they have to be tellable apart at a glance in a list of
   four: waiting (no attribute), uploading, queued, refused. Colour on the
   status text rather than the whole row, so the filename stays legible. */
.stage-row[data-state="busy"]{opacity:.72}
.stage-row[data-state="busy"] .stage-status{color:var(--indigo-500)}
.stage-row[data-state="done"] .stage-status{color:var(--status-success)}
.stage-row[data-state="done"]{border-left:3px solid var(--status-success)}
.stage-row[data-state="error"] .stage-status{color:var(--status-danger)}
.stage-row[data-state="error"]{border-left:3px solid var(--status-danger);
  background:var(--red-50)}
@media (max-width:640px){
  /* The select stops fitting beside the name long before the row does. */
  .stage-row{flex-wrap:wrap}
  .stage-mode{width:100%;min-width:0}
}
/* Sign-out is the one destructive control in the topbar; it reads as neutral
   until you are about to press it. */
.iconbtn-danger:hover{background:var(--red-50);color:var(--status-danger)}
/* Its own scroll container, so the rail can follow a position rather than the
   whole window's.

   flex-grow inside .doc-pane rather than a viewport height, so it fills the
   grid row the PDF frame beside it defines and the two panes are exactly the
   same height — which is the point of putting them side by side. min-height:0
   is what lets a flex item shrink below its content and actually scroll; the
   px floor is a backstop for any context where the viewport measures zero and
   a vh-derived height would collapse the pane to its padding. */
.doc-scroll{flex:1 1 0;min-height:0;overflow:auto}
/* Deliberately NO scroll-behavior:smooth. A rail click is navigation — you have
   named a page and want to be on it — and an animated scroll buys nothing for a
   jump of one screen. It also cost correctness: both spellings of the animated
   scroll (this property, and scrollTo({behavior:"smooth"}) in reader.js) were
   dropped outright by an engine we tested in, leaving the rail marking a page
   the panes had not moved to. A plain scrollTop assignment always lands. */
.doc-pane{min-height:320px}
/* Only when there is no PDF to set the row height: then the text pane is on its
   own and needs a ceiling of its own, or a long document makes the page scroll
   twice. */
.doc-split:not(.has-doc) .doc-scroll{max-height:min(72vh,780px)}
/* A page boundary inside the continuous text. Sticky so you can always see
   which page you are in without scrolling back to its heading.

   Full-bleed, and that is the fix rather than the flourish. It was sticky
   before with `padding:6px 0` and no horizontal reach, so the pane's own 24px
   padding stayed uncovered on both sides and text scrolled up through the gap
   beside the heading — which reads exactly like a header clipping the line
   underneath it. The negative margins pull it out to the pane's edges, the
   padding puts the text back where it was, and the hairline gives the bar an
   edge so the line passing under it looks deliberate.

   z-index:2 clears the per-page copy button's focus ring as well as the text. */
/* Its height, named once, because the page headings have to stick *below* it
   rather than under it. Fixed rather than measured: .seg is 26px, the bar adds
   10px of padding either side and a 1px rule, and all three are set here. */
:root{--segbar-h:47px}
.tabpane-text.has-versions .doc-page-mark{top:var(--segbar-h)}
.doc-page-mark{position:sticky;top:0;z-index:2;margin:22px -24px 8px;
  padding:8px 24px;display:flex;align-items:baseline;justify-content:space-between;
  gap:10px;background:var(--white);border-bottom:1px solid var(--border-subtle);
  font-family:var(--font-display);font-size:var(--text-xs);
  font-weight:var(--weight-semibold);color:var(--text-subtle);
  letter-spacing:var(--tracking-wide);text-transform:uppercase}
.doc-page-mark>span{display:flex;align-items:baseline;gap:10px;min-width:0}
.doc-page-mark:first-child{margin-top:0}
/* The per-page copy button. Faint until the pointer or the keyboard is in that
   page, because there is one of these per page and a column of solid buttons
   down the margin competes with the text they are next to. */
.copy-page{opacity:0;flex:0 0 auto;transition:opacity var(--dur-fast) var(--ease-standard)}
section:hover>.doc-page-mark .copy-page,
.doc-page-mark:hover .copy-page,
.copy-page:focus-visible{opacity:1}
@media (hover:none){.copy-page{opacity:.55}}
.result-grid{display:grid;grid-template-columns:88px minmax(0,1fr) 300px;gap:20px;
  max-width:var(--container-2xl)}
/* The panes grow with the viewport rather than stopping at a fixed height:
   on a tall display the useful thing is to see more of the page at once. The
   min() keeps them from outgrowing the window on a short one. */
@media (min-width:1500px){
  .doc-split.has-doc .doc-pane{height:min(78vh,1000px)}
  .doc-split:not(.has-doc) .doc-scroll{max-height:min(78vh,1000px)}
}
.page-rail{display:flex;flex-direction:column;gap:10px}
.page-thumb{display:block;width:64px;padding:0;border:1px solid var(--border-subtle);
  border-radius:var(--radius-thumb);background:var(--white);box-shadow:var(--shadow-xs);
  cursor:pointer;overflow:hidden;text-decoration:none}
.page-thumb[aria-current="page"]{border-color:var(--indigo-500);box-shadow:var(--shadow-focus)}
.page-thumb-body{height:84px;background:var(--surface-sunken);display:flex;
  flex-direction:column;gap:4px;padding:8px}
.page-thumb-body span{height:3px;background:var(--ink-300);border-radius:2px}
.page-thumb-n{font-family:var(--font-mono);font-size:10px;color:var(--text-subtle);
  padding:4px 0;text-align:center}
.meta-list{display:flex;flex-direction:column}
.meta-row{display:flex;justify-content:space-between;gap:12px;padding:5px 0;
  font-size:var(--text-xs)}
.meta-row .k{color:var(--text-muted)}
.meta-row .v{font-family:var(--font-mono);color:var(--text-strong);text-align:right}
.meta-head{font-size:var(--text-2xs);text-transform:uppercase;
  letter-spacing:var(--tracking-caps);color:var(--text-subtle);margin:0 0 10px}

/* ---- auth pages ---- */
body.auth-body{display:grid;place-items:center;min-height:100vh;padding:24px;overflow:auto}
.auth{width:100%;max-width:420px}
.auth-brand{display:flex;justify-content:center;margin-bottom:20px}
.auth-foot{text-align:center;margin-top:18px;font-size:var(--text-sm);color:var(--text-muted)}

/* `hidden` has to win, and by default it does not. The UA sheet's
   [hidden]{display:none} is in the user-agent origin, which any author rule
   beats — so `.alert` below kept an empty, hidden banner in the flow as a
   12px-padded flex box. That was the gap pushing the whole upload column below
   the cards beside it. */
.alert[hidden]{display:none}
.alert{display:flex;gap:10px;padding:12px 14px;border-radius:var(--radius-sm);
  font-size:var(--text-sm);margin-bottom:16px;align-items:flex-start}
.alert .icon{margin-top:2px;flex:0 0 auto}
.alert-danger{background:var(--status-danger-soft);color:var(--red-600)}
.alert-success{background:var(--status-success-soft);color:var(--teal-600)}
.alert-info{background:var(--surface-brand-soft);color:var(--indigo-600)}
.alert-warning{background:var(--status-warning-soft);color:var(--amber-600)}

.empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:56px 24px;
  text-align:center;color:var(--text-muted)}
.empty h3{font-family:var(--font-display);font-size:var(--text-md);color:var(--text-strong);
  margin:0}
.empty p{font-size:var(--text-sm);max-width:44ch;margin:0}

/* ---- the two toggles inside a reading pane ---- */
/* A segmented control, driven by the same radio-and-label mechanism as the tab
   strip above it: no script, no nonce, and it keeps working with JavaScript
   off. Sticky to the top of its pane so the control that decides what you are
   reading does not scroll away from what you are reading. */
/* THE PANE OWNS NO PADDING; ITS CHILDREN DO, and that is not tidiness. A
   position:sticky child of a scroll container that has padding sticks below
   that padding, not at the container's own top edge — so a bar meant to sit
   flush at the top of the pane rendered 24px down it and the first line of the
   document ran underneath. Moving the padding inward makes the pane's edge and
   the sticky origin the same place. */
.tabpane-text,.tabpane-md,.tabpane-json{padding:0}
.tabpane-text>.ver,.tabpane-md>.ver{padding:24px}
.segbar{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:4px;
  margin:0;padding:10px 24px;background:var(--white);
  border-bottom:1px solid var(--border-subtle);flex-wrap:wrap}
.segbar-gap{flex:1 1 auto;min-width:8px}
.seg{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;
  border-radius:var(--radius-pill);border:1px solid transparent;cursor:pointer;
  font-size:var(--text-xs);color:var(--text-muted);white-space:nowrap;
  transition:var(--transition-control)}
.seg:hover{background:var(--ink-50);color:var(--text-body)}
/* The chosen half of each pair. One rule per radio rather than a generic one,
   for the same reason the tab rules are written out: a pane added without a
   matching rule then fails visibly (nothing looks selected) instead of quietly
   highlighting the wrong control. */
#textver-orig:checked~.tabpane-text .seg[for=textver-orig],
#textver-ai:checked~.tabpane-text .seg[for=textver-ai],
#mdver-orig:checked~.tabpane-md .seg[for=mdver-orig],
#mdver-ai:checked~.tabpane-md .seg[for=mdver-ai],
#mdview-rendered:checked~.tabpane-md .seg[for=mdview-rendered],
#mdview-raw:checked~.tabpane-md .seg[for=mdview-raw]{
  background:var(--surface-brand-soft);color:var(--indigo-600);
  border-color:var(--indigo-100);font-weight:var(--weight-medium)}

/* Which version of a document is on screen, and which view of it. Hidden by
   default and shown by a rule, never the other way round: a block added without
   a rule then shows nothing rather than showing two versions at once, and two
   versions at once is the failure that would matter — a customer reading the
   rewrite while believing it is the extraction. */
.ver{display:none}
#textver-orig:checked~.tabpane-text .ver-orig,
#textver-ai:checked~.tabpane-text .ver-ai,
#mdver-orig:checked~.tabpane-md .ver-orig,
#mdver-ai:checked~.tabpane-md .ver-ai{display:block}

/* Raw and rendered, and they are hidden by two different mechanisms on purpose.
   The rendered preview is simply removed. The raw source is only *clipped* —
   it stays rendered, occupying one pixel — because it is what the Copy button
   reads in both views, and a preview whose source you cannot copy is the one
   thing this tab must not become. reader.js picks the copy target by asking
   which candidate has layout boxes, and `display:none` would leave it choosing
   between two sources that both have none: the extraction's and the rewrite's,
   with no way to tell which version is on screen. Clipping keeps exactly one of
   them measurable, which is the one inside the visible `.ver`. */
.tabpane-md .mdview-rendered{display:none}
#mdview-rendered:checked~.tabpane-md .mdview-rendered{display:block}
.tabpane-md .mdview-raw{position:absolute;width:1px;height:1px;padding:0;
  margin:-1px;overflow:hidden;clip-path:inset(50%)}
#mdview-raw:checked~.tabpane-md .mdview-raw{position:static;width:auto;
  height:auto;margin:0;overflow:auto;clip-path:none}
/* No second version to pick between: the only one there is, is the one shown.
   Without this a document that has never been through the second pass renders
   an empty Text tab, because the rules above all hang off radios the template
   did not emit. */
.tabpane-text:not(.has-versions) .ver-orig,
.tabpane-md:not(.has-versions) .ver-orig{display:block}

/* The rendered Markdown. Deliberately modest: this is a preview of a document,
   not a document, and it sits in a pane 300px wide at the narrow end of the
   split. Everything here is a token the rest of the app already uses. */
.markdown-body{font-size:var(--text-sm);line-height:1.65;color:var(--text-body)}
.markdown-body>*+*{margin-top:12px}
.markdown-body h2,.markdown-body h3,.markdown-body h4,
.markdown-body h5,.markdown-body h6{margin-top:22px;color:var(--text-strong)}
.markdown-body h2{font-size:var(--text-md)}
.markdown-body h3{font-size:var(--text-base)}
.markdown-body h4,.markdown-body h5,.markdown-body h6{font-size:var(--text-sm)}
.markdown-body ul,.markdown-body ol{margin-left:20px;padding:0}
.markdown-body li+li{margin-top:4px}
.markdown-body code{background:var(--surface-sunken);padding:1px 4px;
  border-radius:var(--radius-xs);font-size:var(--text-xs)}
.markdown-body pre{background:var(--surface-sunken);padding:12px;
  border-radius:var(--radius-sm);overflow-x:auto}
.markdown-body pre code{background:none;padding:0}
.markdown-body blockquote{border-left:3px solid var(--border-default);
  padding-left:12px;color:var(--text-muted)}
.markdown-body hr{border:0;border-top:1px solid var(--border-subtle)}
.markdown-body a{word-break:break-word}
.markdown-body table.data{min-width:0}

/* ---- the validation accordion ---- */
/* Collapsed detail, native. A <details> rather than a script because the
   summary above it is already the whole answer for most readers, and the list
   underneath must still open on a page where JavaScript is blocked. */
.checks{margin-top:12px;border-top:1px solid var(--border-subtle);padding-top:12px}
.checks-summary{display:flex;align-items:center;gap:8px;cursor:pointer;
  list-style:none;font-size:var(--text-xs);color:var(--text-body)}
.checks-summary::-webkit-details-marker{display:none}
.checks-summary .fine{margin-left:auto;font-family:var(--font-mono)}
.checks[open] .checks-summary .icon{transform:rotate(90deg)}
.checks-summary .icon{transition:transform var(--dur-fast) var(--ease-standard)}
.check-list{display:flex;flex-direction:column;gap:10px;margin-top:12px}
.check{display:flex;gap:8px;align-items:flex-start}
.check-mark{flex:0 0 auto;margin-top:2px}
.check-ok .check-mark{color:var(--teal-600)}
.check-bad .check-mark{color:var(--red-600)}
/* Not applicable is its own state and looks like one. Rendering it as a muted
   pass is precisely the confusion validation.py exists to prevent: a check that
   found nothing to test has not agreed with anything. */
.check-skip .check-mark{color:var(--text-subtle)}
.check-skip{opacity:.72}
.check-kind{font-size:var(--text-xs);font-weight:var(--weight-medium);
  color:var(--text-strong);margin:0}
.check-kind .badge{height:16px;padding:0 5px;font-size:var(--text-2xs);
  margin-left:6px}
.check-detail{font-size:var(--text-xs);color:var(--text-muted);margin:2px 0 0;
  line-height:1.5}
.check-evidence{display:flex;flex-direction:column;gap:2px;margin:6px 0 0}
.check-evidence span{font-family:var(--font-mono);font-size:var(--text-2xs);
  color:var(--text-subtle);background:var(--surface-sunken);padding:2px 6px;
  border-radius:var(--radius-xs);overflow-x:auto;white-space:pre}

/* ---- two-step destructive controls, and the offered-fix panel ---- */
/* Both are a <details> whose summary is styled as a button. The reason it is
   not a confirm() dialog: this has to work with scripts blocked, and the worst
   possible version of a delete control is one that silently does nothing. */
.confirm summary,.offered summary,.rename summary{list-style:none;cursor:pointer}
.confirm summary::-webkit-details-marker,
.offered summary::-webkit-details-marker,
.rename summary::-webkit-details-marker{display:none}
.confirm-body,.offered-body{margin-top:10px;padding:12px;
  background:var(--surface-sunken);border-radius:var(--radius-sm)}
.confirm-body .fine{margin:0 0 10px;display:block}
/* The greyed Improve format button. Styled as disabled and deliberately NOT
   disabled: a disabled control cannot be clicked, and clicking it is how the
   customer finds out why it is off and, in the one case where they can, turns
   it on. */
.btn.is-off{background:var(--action-disabled);color:var(--action-disabled-text);
  border-color:transparent;box-shadow:none}
.btn.is-off:hover{background:var(--ink-200);color:var(--text-muted)}
.offered[open] summary.is-off{background:var(--ink-200)}

/* ---- rename ---- */
.rename-summary{display:flex;align-items:flex-start;justify-content:space-between;
  gap:8px}
.rename-summary .card-title{word-break:break-word}
.rename-summary .iconbtn{flex:0 0 auto;opacity:.5}
.rename-summary:hover .iconbtn{opacity:1;color:var(--indigo-500)}
.rename[open] .rename-summary .iconbtn{opacity:1}

/* One card per page, all rendered, one shown — reader.js moves the attribute as
   you scroll. The explicit rule is needed because [hidden] lives in the
   user-agent origin and .card is an author rule; the same trap .alert hit. */
.page-meta[hidden]{display:none}

/* ---- responsive ---- */
@media (max-width:900px){
  .app-grid,.result-grid{grid-template-columns:1fr}
  .page-rail{flex-direction:row;flex-wrap:wrap}
}
@media (max-width:720px){
  .app{flex-direction:column;height:auto}
  body.app-body{overflow:auto}
  .sidebar{width:100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px;
    border-right:0;border-bottom:1px solid var(--border-subtle);padding:8px 16px;
    overflow-x:auto}
  .sidebar-brand{height:auto;padding:0 8px 0 0}
  .sidebar-nav{flex-direction:row;margin-top:0;gap:4px}
  .sidebar-section,.sidebar-foot{display:none}
  .app-main{padding:20px 16px}
  .topbar{padding:0 16px}
}
