/* Layout and component styling. Colours live in theme.css. */

* { box-sizing: border-box; }

/* `100%` alone resolves against the browser's *layout* viewport, which on
   iOS Safari is taller than the area actually visible when its address bar
   and toolbar are showing — the fixed tab bar (`.tab-bar`, CF-MOB-004) then
   sits below the fold until a scroll collapses that chrome. `100dvh` tracks
   the visible area as the toolbar shows/hides; `100%` stays as the fallback
   for a browser that does not support the dynamic unit. */
html, body {
  height: 100%;
  height: 100dvh;
  margin: 0;
  overflow-x: hidden; /* nothing here is meant to scroll sideways at all */
}

body {
  background: var(--page-bg);
  color: var(--text);
  font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  overflow: hidden; /* the two panes scroll, the page does not */
}

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

/* ---- Overall layout (CF-LAY-001, CF-LAY-002) ------------------------- */

#app {
  display: grid;
  grid-template-columns: var(--bar-width) 1fr;
  height: 100%;
  height: 100dvh;
}

/* Below the mobile width threshold, `TabBar` hides every panel but the
   active one (CF-MOB-001) — the desktop two-column grid would otherwise
   still reserve `--bar-width` for a `.bar` that is empty air half the time,
   and give the visible panel only what is left. One column, one panel
   showing at a time. `:has()` rather than a class on `#app`: the tab bar
   already carries its own placement as a class (`.tab-bar-bottom` /
   `.tab-bar-rail`), and stating the mobile condition once there and reusing
   it here avoids the two ever disagreeing about what "mobile" means. */
#app:has(.tab-bar:not([hidden])) {
  grid-template-columns: 1fr;
}

.bar {
  display: flex;
  flex-direction: column;
  min-height: 0;              /* or the label area cannot scroll */
  background: var(--panel-bg);
  border-right: 1px solid var(--panel-border);
}

.results-pane {
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.25rem 4rem;
}

/* `.bar`'s and `.results-pane`'s own `display` (flex, block) would otherwise
   win over the UA stylesheet's `[hidden] { display: none }` — an attribute
   selector has the same specificity as a class one, and these are declared
   later, so without this a panel `TabBar` hides (`../ui/tabBar.js`) stays
   laid out and visible underneath the one meant to replace it. */
.bar[hidden], .results-pane[hidden], .settings-panel[hidden] { display: none; }

/* The desktop cog (`.settings`, `src/ui/settings.js`) and the mobile
   Settings tab (`.settings-panel`) are two presentations of the same theme
   choice, never both reachable at once: whichever the tab bar is showing has
   already answered "where do I change the theme", and a cog surviving at the
   foot of the Search tab's sidebar would be a second, redundant answer sitting
   below a scroll a thumb might not even reach. */
#app:has(.tab-bar:not([hidden])) .settings { display: none; }

/* ---- Mobile tab bar (CF-MOB-001 … CF-MOB-006) ------------------------- */

/* Room for the bar is reserved with padding on the panels rather than the
   bar overlapping them, unlike the viewer's overlay (`.viewer` is `position:
   fixed`) — a tab bar covering the last row of results or the bottom of the
   label area would hide exactly the content a thumb is about to reach for. */
#app:has(.tab-bar-bottom) .bar,
#app:has(.tab-bar-bottom) .results-pane,
#app:has(.tab-bar-bottom) .settings-panel {
  padding-bottom: calc(0.75rem + var(--tab-bar-size, 3.2rem));
}

#app:has(.tab-bar-rail) .bar,
#app:has(.tab-bar-rail) .results-pane,
#app:has(.tab-bar-rail) .settings-panel {
  padding-left: calc(0.75rem + var(--tab-bar-size, 3.2rem));
}

.tab-bar {
  position: fixed;
  z-index: 10;
  display: flex;
  background: var(--panel-bg);
  border-color: var(--panel-border);
}

.tab-bar[hidden] { display: none; }

.tab-button {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;         /* a target a thumb can hit without aiming */
  border: 0;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.tab-button.selected {
  color: var(--accent);
  font-weight: 600;
}

/* A phone held upright: the bar spans the bottom edge, tabs share the width. */
.tab-bar:not(.tab-bar-rail) {
  left: 0;
  right: 0;
  bottom: 0;
  flex-direction: row;
  border-top: 1px solid var(--panel-border);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* A phone on its side: the bar stands on the left edge, tabs share the
   height. Sized by its own label text (`width: max-content`) rather than a
   fixed guess — three short words is cheap to fit, unlike CasaFluxFE's seven
   pages, which had to drop labels here entirely to stay a thumb-width strip.
   Keeping the labels is worth the extra few rem: "which tab am I in" stays
   answered without learning icons that don't exist yet (§Framework, no icon
   set built). */
.tab-bar-rail {
  left: 0;
  top: 0;
  bottom: 0;
  flex-direction: column;
  width: max-content;
  border-right: 1px solid var(--panel-border);
  padding-left: env(safe-area-inset-left, 0px);
}

.tab-bar-rail .tab-button {
  padding: 0 1.1rem;
}

.settings-panel {
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.25rem;
}

.settings-panel-head {
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.settings-panel .settings-themes { gap: 0.5rem; }
.settings-panel .settings-theme { font-size: 1rem; padding: 0.5rem 0.4rem; }

/* A second radio group (language) below the first (theme) in the same
   popover/panel — a plain heading, unlike `.settings-popover-head`, which
   is laid out `space-between` for its close button and only belongs on the
   very first heading. */
.settings-popover .settings-section-head {
  margin-top: 1rem;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
}
.settings-panel .settings-section-head { margin-top: 1rem; }

/* ---- Search field (CF-SRCH-001 … CF-SRCH-003) ------------------------ */

.search-area {
  padding: 0.75rem;
  border-bottom: 1px solid var(--panel-border);
}

/* The field is one editable line. Recognised tokens are drawn as pills in a
   layer *behind* a plain text input, rather than as elements the input has to
   flow around — the caret, selection, undo and native text editing all have to
   keep working, and only a real input gives that. The two layers share the same
   font metrics and padding so the drawn tokens sit exactly under their
   characters. */
.search-field {
  position: relative;
  display: block;
  padding: 0;
  min-height: 2.5rem;
  background: var(--page-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  cursor: text;
  overflow: hidden;
}

.search-field:focus-within { border-color: var(--accent); }

/* A line that cannot be searched with yet is marked, and the results go on
   showing the last question that was finished (CF-SRCH-045). */
.search-field.incomplete { border-color: var(--warn); }

.pill-underlay,
.search-input {
  /* Any difference here shows up as drift between a token and its text. */
  font: inherit;
  font-size: 0.95rem;
  line-height: 1.6;
  padding: 0.45rem 0.6rem;
  white-space: pre;
  letter-spacing: normal;
}

.pill-underlay {
  position: absolute;
  inset: 0;
  overflow: hidden;
  color: transparent;          /* the input draws the text; this draws behind it */
  pointer-events: none;
  user-select: none;
}

.search-input {
  position: relative;
  display: block;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--text);
  overflow-x: auto;
}
.search-input:focus { outline: none; }

/* Token backgrounds. Drawn on the underlay, so they sit behind the glyphs.
   The field's own background is `--page-bg`, so these must read against that
   rather than against the panel — `--pill-bg` is almost exactly `--page-bg` in
   the dark theme and the highlighting was invisible. */
.tok {
  border-radius: 999px;
  padding: 0.05rem 0;
  box-decoration-break: clone;
}
/* The spread is capped below the single space of real text separating two
   terms (this file's own note above: "the gap between terms is a single
   space, narrower than any button") — at 0.28em two adjacent pills' spreads
   overlapped that space entirely and touched, reported directly by the
   user from a screenshot. 0.2em leaves a visible gap between pills while
   still reading as padding around one, not a name printed on bare
   background — the alignment this comment's caller enforces is with the
   underlying text's box geometry, which this value does not touch. */
.tok-label,
.tok-quoted {
  background: var(--tok-label-bg);
  box-shadow: 0 0 0 0.2em var(--tok-label-bg);
}
.tok-date {
  background: var(--tok-date-bg);
  box-shadow: 0 0 0 0.2em var(--tok-date-bg);
}
/* Operators and brackets are structure, not content. They are left as plain
   text so the eye reads the pills as the terms and the rest as the shape of
   the question. */
.tok-operator,
.tok-open,
.tok-close { font-weight: 700; }
/* A word naming no known label, and a date still being typed: both are marked
   so the user can see the field did not recognise them yet. */
.tok-text { border-bottom: 2px wavy var(--danger); }
.tok-partial-date { border-bottom: 2px dashed var(--warn); }

/* The terms of the current line, listed below the field so each can be removed
   with one press (CF-SRCH-050).

   Not an `x` drawn on the token itself: the underlay has to stay glyph-for-glyph
   aligned with the input beneath it, so nothing there may occupy width. A button
   placed inside a token covers its last characters, and one hanging past the
   right edge lands on the following word — the gap between terms is a single
   space, narrower than any button. Both were built and both were unreadable. */
/* A term the user has pressed. One at a time, so only one close control has to
   exist — which is what lets it fit in a line whose drawn text must stay aligned
   with the text typed beneath it. */
.tok-selected {
  position: relative;
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 999px;
}

/* The control sits just past the term's last character, in the gap the
   following space already provides. It is `position: absolute`, so it occupies
   no width of its own and pushes nothing out of register. */
.tok-x {
  position: absolute;
  top: 50%;
  left: 100%;
  transform: translate(0.15em, -50%);
  width: 1.15em;
  height: 1.15em;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-text);
  font: inherit;
  font-size: 0.8em;
  line-height: 1;
  cursor: pointer;
  z-index: 4;
}

.tok-x:hover { background: var(--danger); }

/* An operator left with nothing to work on. Marked rather than removed: which
   side of an `and` the user meant to keep is not knowable (CF-SRCH-054). */
.tok-dangling {
  color: var(--danger);
  text-decoration: underline wavy var(--danger);
  text-underline-offset: 0.2em;
}

.search-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.linkish {
  border: 0;
  background: none;
  padding: 0;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
}

/* ---- Suggestions (CF-SRCH-004, CF-SRCH-029) -------------------------- */

.suggest-wrap { position: relative; }

.suggestions {
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  margin-top: 0.25rem;
  max-height: 17rem;
  overflow-y: auto;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px var(--thumb-shadow);
  padding: 0.2rem;
}

.suggestion {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.3rem 0.45rem;
  border: 0;
  border-radius: calc(var(--radius) - 0.2rem);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.suggestion[aria-selected="true"] { background: var(--accent-soft); }
.suggestion:hover { background: var(--accent-soft); }

.suggestion .count { color: var(--text-faint); font-variant-numeric: tabular-nums; }

/* An approximate match is worth showing differently from one actually typed;
   the ordering already keeps the two groups apart (CF-SRCH-029). */
.suggestion.approx .name { color: var(--text-dim); font-style: italic; }

.suggest-group {
  padding: 0.35rem 0.45rem 0.2rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}

/* ---- Date range (CF-DATE-001 … CF-DATE-007, CF-DATE-013) ------------- */

.date-range {
  position: relative;
  padding: 0.7rem 0.75rem 0.5rem;
  border-bottom: 1px solid var(--panel-border);
}

.date-range-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.date-range-label { font-variant-numeric: tabular-nums; }

/* When a range is set it is the answer to a question the user asked, so it
   reads as active rather than as a caption. */
.date-range.filtered .date-range-label {
  color: var(--accent);
  font-weight: 600;
}

/* Only worth offering once the range is actually narrower than the full
   span — otherwise there is nothing for it to reset. */
.date-range-reset-range {
  display: none;
  font-size: inherit;
}

.date-range.filtered .date-range-reset-range { display: inline; }

.date-quick-filters {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
  margin-left: auto;
  /* "latest(" and ")" name what the links inside mean; they are text, not
     controls, so they share the row's dim caption colour rather than the
     link colour the day/week/month buttons use. */
  color: var(--text-dim);
}

.date-quick-filter-links {
  display: flex;
  gap: 0.4rem;
}

.date-quick-filter { font-size: inherit; }

/* A date term already in the search text (CF-DATE-013): the track, dots,
   year-range label and quick-filter links give way to a single "Reset date"
   control. Hidden with visibility/opacity rather than display, so the panel
   keeps its height and the label area below it does not jump. */
.date-range.date-taken .date-range-label,
.date-range.date-taken .date-quick-filters,
.date-range.date-taken .date-track,
.date-range.date-taken .date-ticks {
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

/* Centred over the whole panel, not just the head row, since everything
   else in the panel is hidden while this shows — an absolute overlay is
   what makes "centred both ways" mean the panel as a whole rather than the
   thin row this button happens to share markup with. */
.date-reset {
  position: absolute;
  inset: 0;
  margin: auto;
  width: fit-content;
  height: fit-content;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

.date-range.date-taken .date-reset {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
}

.date-reset:disabled {
  cursor: default;
  opacity: 0.5;
  text-decoration: none;
}

.date-track {
  position: relative;
  height: 2.1rem;
  /* Room for a dot's own half-width (see `.date-dot`'s `margin-left:
     -0.425rem`) at each end of the track, so a dot sitting at its year's 0%
     or 100% position does not have its outer half sitting flush against the
     sidebar's own edge — on a phone that put the rightmost dot right at the
     screen border, too close to grab without also catching the edge-swipe
     gesture the browser reads there. */
  margin: 0 0.5rem;
  cursor: pointer;
  touch-action: none;       /* or a drag scrolls the sidebar instead */
}

.date-histogram {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.date-rail {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--pill-border);
}

.date-selected {
  position: absolute;
  bottom: 0;
  height: 2px;
  background: var(--accent);
}

.date-dot {
  position: absolute;
  bottom: 1px;
  width: 0.85rem;
  height: 0.85rem;
  margin-left: -0.425rem;
  transform: translateY(50%);
  padding: 0;
  border: 2px solid var(--panel-bg);
  border-radius: 50%;
  background: var(--accent);
  cursor: grab;
  box-shadow: 0 1px 3px var(--thumb-shadow);
}

.date-dot:hover { transform: translateY(50%) scale(1.15); }
.date-dot.dragging { cursor: grabbing; transform: translateY(50%) scale(1.2); }

.date-ticks {
  position: relative;
  height: 1rem;
  margin-top: 0.15rem;
}

/* Ticks are positioned where their year actually sits on the compressed axis,
   not at even intervals along the track. */
.date-tick {
  position: absolute;
  transform: translateX(-50%);
  font-size: 0.66rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- Label area (CF-LBL-001 … CF-LBL-007) ---------------------------- */

.label-area {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 0.75rem;
}

.label-area-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.5rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}

/* Inline, flowing like text — not a single-column list (CF-LBL-002). */
.label-flow {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

/* ---- Settings (CF-SET-001 … CF-SET-005) ------------------------------ */

/* A plain flex child after `.label-area`, not `position: sticky` or fixed —
   `.label-area` already takes `flex: 1` and scrolls within the remaining
   space (CF-LAY-002), so this row is naturally pinned below it without the
   cog ever entering the scrolling content. A row rather than a bare cog since
   CF-DOC-010: the unparseable-documents link sits centred in the same strip,
   the cog pinned to its side. */
.settings {
  position: relative;         /* the popover is positioned against this */
  flex: none;
  display: flex;
  align-items: center;
  border-top: 1px solid var(--panel-border);
  padding: 0.4rem;
}

/* Centred by taking the row's remaining space either side of the cog
   (`flex: 1` with no sibling before it), rather than `text-align: center` —
   that would centre within the row's own content width, not the row itself,
   which is not the same thing once the cog's width is subtracted. */
.unparseable-link {
  flex: 1;
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 0.85rem;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  cursor: pointer;
  text-align: center;
}

.unparseable-link:hover { background: var(--accent-soft); color: var(--text); }

.settings-cog {
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  cursor: pointer;
}

.settings-cog:hover,
.settings-cog[aria-expanded="true"] { background: var(--accent-soft); color: var(--text); }

/* Anchored above the cog rather than below it: the cog sits at the very
   bottom of the sidebar, and a popover opening downward would be drawn
   outside the viewport. */
.settings-popover {
  position: absolute;
  z-index: 20;
  left: 0.4rem;
  bottom: 100%;
  margin-bottom: 0.4rem;
  width: 16rem;
  max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px var(--thumb-shadow);
  padding: 0.6rem;
}

.settings-popover-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
}

.settings-close {
  border: 0;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}

.settings-close:hover { color: var(--danger); }

.settings-themes {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.settings-theme {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.3rem;
  border-radius: calc(var(--radius) - 0.2rem);
  font-size: 0.88rem;
  cursor: pointer;
}

.settings-theme:hover { background: var(--accent-soft); }

/* A plain text link below the theme/language sections, not another radio
   group — `display: block` gives it the same section-to-section spacing the
   radio groups get from `.settings-section-head`'s own margin, without
   sharing that class's bold heading look, which a clickable link should not
   have. */
.settings-run-log {
  display: block;
  margin-top: 1rem;
  font-size: 0.85rem;
}

/* ---- Run log modal ---------------------------------------------------- */

/* Centred overlay, not a small anchored popover: the log's content is prose
   meant to be read rather than a short list of choices, so it gets the
   viewer's own backdrop-and-centred-box shape instead of `.settings-popover`'s
   anchored one. */
.run-log-modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--on-document-backdrop);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.run-log-modal[hidden] { display: none; }

.run-log-panel {
  width: 40rem;
  max-width: 100%;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px var(--thumb-shadow);
}

.run-log-head {
  flex: none;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--panel-border);
  font-weight: 600;
}

.run-log-close {
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 1.2rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}

.run-log-close:hover { color: var(--danger); }

.run-log-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 1rem;
}

.run-log-text {
  margin: 0;
  font-family: ui-monospace, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ---- Reconnect overlay (CF-OFF-003 … CF-OFF-005) ---------------------- */

/* Covers and dims the whole interface — a higher z-index than the viewer
   and the run-log modal, since a client that cannot reach the backend has
   nothing underneath worth interacting with, whatever else happened to be
   open when the connection was lost. No control is offered here (CF-OFF-003):
   there is no local action that changes when the backend answers again. */
.reconnect-overlay {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--on-document-backdrop);
  color: var(--on-document-text);
}

.reconnect-overlay[hidden] { display: none; }

.reconnect-message {
  font-size: 1.1rem;
}

/* ---- Update prompt (CF-OFF-007 … CF-OFF-010) --------------------------- */

.update-prompt {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--on-document-backdrop);
  padding: 1.5rem;
}

.update-prompt[hidden] { display: none; }

.update-panel {
  width: 24rem;
  max-width: 100%;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px var(--thumb-shadow);
  padding: 1.25rem;
}

.update-title {
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.update-body {
  margin: 0 0 1rem;
  color: var(--text-dim);
  font-size: 0.9rem;
  line-height: 1.4;
}

.update-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 1rem;
}

.update-restart {
  border: 0;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-text);
  padding: 0.45rem 0.9rem;
  font: inherit;
  cursor: pointer;
}

.update-restart:hover { filter: brightness(1.08); }

/* ---- Pills (CF-LBL-009 … CF-LBL-012) --------------------------------- */

scanbox-pill {
  position: relative;         /* the `x` is positioned against this */
  display: inline-flex;
  align-items: stretch;
  max-width: 100%;
  border-radius: 999px;       /* rounded left and right ends */
  background: var(--pill-bg);
  border: 1px solid var(--pill-border);
  overflow: hidden;
  font-size: 0.82rem;
  line-height: 1.6;
}

scanbox-pill .pill-body {
  display: inline-flex;
  align-items: stretch;
  gap: 0.3rem;
  min-width: 0;
  color: var(--pill-text);
  font: inherit;
  cursor: pointer;
  /* A press-and-hold on this element is a gesture the app reads itself
     (CF-LBL-026), not a request to select its text. Without this the browser's
     own long-press-to-select still races it: the hold reaches the exclude
     threshold, but the platform's own selection highlight and, on iOS, its
     copy/lookup callout land on top of it at the same time. `-webkit-touch-
     callout` is the iOS-specific half `user-select: none` does not cover. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

scanbox-pill .pill-text {
  padding: 0.05rem 0.1rem 0.05rem 0.6rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Sized by its content, so it is never wider than that number needs
   (CF-LBL-010). Positioned so the `x` can be laid over exactly this chip
   rather than over a guess at its width. */
scanbox-pill .pill-count {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0.05rem 0.45rem;
  color: var(--pill-count-text);
  font-variant-numeric: tabular-nums;
  font-size: 0.92em;
}

/* Only a chip that actually holds a number is painted. */
scanbox-pill .pill-count:not([hidden]) { background: var(--pill-count-bg); }

/* The `x` lives inside the count chip, so it inherits the chip's width and
   position for free — no second copy of the chip's geometry to keep in step. */
scanbox-pill .pill-count .pill-x {
  position: absolute;
  inset: 0;
}

/* The `x` is drawn over the right end of the pill rather than beside it
   (CF-LBL-020), so revealing it changes nothing about the pill's size.

   It used to grow the pill from zero width on hover. That reflows the row, and
   a pill near the right edge no longer fitted and wrapped to the next line —
   out from under the pointer, so hover ended, so it sprang back. The pills that
   could never be dismissed were exactly the rightmost on each row.

   Reserving the space permanently also fixes that and was tried: it cost a
   fifth of the sidebar's density, taking the same fifty labels from twenty rows
   to twenty-five, to hold room for a glyph that is only ever visible under the
   pointer. Overlaying costs nothing and keeps CF-LBL-010 honest — the count
   part stays exactly as wide as its number needs. */
scanbox-pill .pill-x {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  padding-inline: 0.4rem;
  background: var(--pill-count-bg);
  color: var(--pill-count-text);
  font: inherit;
  font-size: 1.05em;
  /* `line-height: 1` would box the glyph tightly and centre that box, not the
     mark. The `×` sits entirely above the baseline — its ink runs from about
     7.2px up to 0.6px up at this size — so centring its line box leaves the
     mark visibly high. Inheriting the pill's line-height gives the glyph the
     same half-leading above and below that the count has, which puts the ink
     where the digits are. */
  line-height: inherit;
  cursor: pointer;
  visibility: hidden;
}

/* The `x` replaces the number rather than covering it: two things stacked in
   one place read as a mistake, and the count is not useful at the moment the
   user has decided to act on the pill (CF-LBL-022).

   The number keeps its space while hidden — `visibility`, not `display` — so
   the pill does not resize (CF-LBL-020), and the `x` stretches across exactly
   that space so the chip stays a whole shape rather than leaving a blank stub
   where the digits were.

   Wrapped in `@media (hover: hover)` rather than left unconditional with a
   later `@media (hover: none)` override: a device with no hover has nothing
   to reveal the `x` with (a long-press reaches the same exclude action
   instead, CF-LBL-026), so it should never gain `.pill-x` visibility in the
   first place, not gain it and then have it taken away by a second rule.
   The override approach was tried and, on iOS Safari, was seen to leave the
   `x` showing on whichever pill a tap's *release point* lands on once the
   label area rebuilds mid-tap (the result of adding/excluding a label,
   `labelArea.js`'s `_render`) — the platform resolves its own tap-driven
   `:focus-within` against the DOM current at release, not the element
   originally pressed, and a same-position replacement pill inherits it.
   `:hover`/`:focus-within` never being live in the CSS at all for this
   device class removes the state for that resolution to land on. */
@media (hover: hover) {
  scanbox-pill:hover .pill-x,
  scanbox-pill:focus-within .pill-x {
    visibility: visible;
  }

  scanbox-pill:hover .pill-number,
  scanbox-pill:focus-within .pill-number {
    visibility: hidden;
  }
}

scanbox-pill .pill-x:hover { color: var(--danger); }
scanbox-pill .pill-x[hidden] { display: none; }

/* A pill with no number still needs its `x`, so the chip stays in the layout
   as an empty, transparent box just wide enough to hold one. It is not
   `display: none`, or the `x` inside it would go with it. */
scanbox-pill .pill-count[hidden] {
  display: flex;
  padding: 0;
  min-width: 1.5rem;
}

/* A pill in the search field: the query, not an offer. */
scanbox-pill[context="search"] {
  background: var(--pill-search-bg);
  border-color: transparent;
}
scanbox-pill[context="search"] .pill-body { color: var(--pill-search-text); }
scanbox-pill[context="search"] .pill-x {
  background: transparent;
  color: var(--pill-search-text);
}
scanbox-pill[context="search"] .pill-count:not([hidden]) {
  background: var(--pill-search-count-bg);
  color: var(--pill-search-count-text);
}

/* An excluded label reads as a negation rather than a selection. */
scanbox-pill[negated] {
  background: var(--pill-not-bg);
  border-color: var(--pill-not-border);
}
scanbox-pill[negated] .pill-body { color: var(--pill-not-text); }
scanbox-pill[negated] .pill-x {
  background: transparent;
  color: var(--pill-not-text);
}
scanbox-pill[negated] .pill-text::before { content: "−"; margin-right: 0.25rem; }

/* ---- Results (CF-RES-001 … CF-RES-006) ------------------------------- */

.results-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin-bottom: 0.9rem;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.results-head .total { color: var(--text); font-size: 1rem; font-weight: 600; }

.thumb-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: 0.9rem;
}

.thumb {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: var(--text-dim);
  cursor: pointer;
  text-align: center;
}

/* Sized to match the canvas/img it wraps, so a thumbnail-label overlay
   (`.thumb-labels`, below) can position against exactly the picture rather
   than the whole tile, which also includes the date underneath. */
.thumb-frame { position: relative; }

.thumb canvas,
.thumb .thumb-img {
  width: 100%;
  aspect-ratio: 1 / 1.414;    /* A4, which is what the archive is */
  display: block;
  background: var(--thumb-bg);
  border: 1px solid var(--thumb-border);
  border-radius: 0.25rem;
  box-shadow: 0 1px 3px var(--thumb-shadow);
}

.thumb:hover canvas { border-color: var(--accent); }

.thumb .date {
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}

/* ---- Thumbnail labels (CF-RES-018 … CF-RES-021) ----------------------- */

/* Only the labels not already in the current search are ever drawn — the
   searched-for labels are on every result and would add nothing
   (CF-RES-019). Small pills rather than the label area's full pill treatment
   (`scanbox-pill`): chosen over "small text" and "a corner badge" after
   comparing all three live against real archive data (TASKS.md, "Labels on
   thumbnails") — text stayed compact but truncated illegibly on a
   heavily-labelled document, and a badge hid the labels behind a hover/tap
   that a text or pill treatment does not need. */

.thumb-labels {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem;
  padding: 0.3rem;
  background: linear-gradient(to top, var(--on-document-scrim), transparent);
  border-radius: 0 0 0.25rem 0.25rem;
  pointer-events: none;      /* the tile underneath stays clickable */
}

.thumb-label-pill {
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  background: var(--on-document-pill-bg);
  color: var(--on-document-pill-text);
  font-size: 0.6rem;
  line-height: 1.4;
  white-space: nowrap;
}

.empty, .failure {
  padding: 2rem 0;
  color: var(--text-dim);
  max-width: 34rem;
}

.failure { color: var(--danger); }

.sentinel { height: 1px; }

.loading {
  padding: 1rem 0;
  color: var(--text-faint);
  font-size: 0.85rem;
}

/* ---- Unparseable documents (CF-DOC-006 … CF-DOC-010) ------------------ */

/* Reuses `.results-head`'s layout; only the second child differs (a plain
   count there, a back link here) — `margin-left: auto` pushes it to the
   row's far side without a second flex rule set. */
.unparseable-pane .unparseable-back { margin-left: auto; }

.unparseable-list {
  display: flex;
  flex-direction: column;
}

/* A `<button>`, not a plain row (CF-DOC-011: every unparseable document can
   be opened in the viewer, unconditionally, since every one carries an `id`)
   — the reset undoes the button-ness visually so it still reads as a plain
   list row at rest, and `:hover`/`:focus-visible` is what actually signals
   "this opens something," the same idiom `.thumb` uses in the results grid. */
.unparseable-row {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--panel-border);
  background: none;
  font: inherit;
  text-align: left;
  padding: 0.45rem 0.3rem;
  cursor: pointer;
}

.unparseable-row:hover,
.unparseable-row:focus-visible { background: var(--accent-soft); }

.unparseable-name {
  font-size: 0.88rem;
  color: var(--text);
  overflow-wrap: anywhere;   /* a raw filename has no spaces to wrap at */
}

/* The dog-eared-sheet silhouette `drawPlaceholder` draws on a results-grid
   tile (`placeholder.js`), at list size — `currentColor` (set inline, per
   `fileTypeGlyph`) drives both the outline and the label so the two always
   match without the SVG repeating the colour. */
.filetype-glyph {
  flex: none;
  position: relative;
  width: 1.4rem;
  height: 1.7rem;
}

.filetype-glyph svg { width: 100%; height: 100%; }

.filetype-glyph-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.2rem;
  text-align: center;
  font-size: 0.4rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  /* Longer fallback type names (`typeLabel`'s subtype-derived tail, up to 4
     characters) must not overflow the glyph's width. */
  overflow: hidden;
  white-space: nowrap;
}

/* ---- Viewer (CF-VIEW-001 … CF-VIEW-012) ------------------------------- */

/* No permanent top bar (unlike the earlier design): the date and labels live
   behind `.viewer-info-toggle` instead, and previous/next are a gesture
   rather than a button pair — both traded for screen room, since the viewer
   is often the smallest window on the smallest device this runs on. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--on-document-backdrop);
}

.viewer[hidden] { display: none; }

.viewer-close {
  position: absolute;
  /* `calc(... + env(...))` rather than plain `0.75rem`: on a phone the viewer
     (`position: fixed; inset: 0`, above) runs up under the status bar, and a
     control placed only `0.75rem` down from the true top edge sits half
     behind it — `env(safe-area-inset-top)` is the status bar's own height
     (0 on a device with none, so this is exactly `0.75rem` there too). */
  top: calc(0.75rem + env(safe-area-inset-top, 0px));
  right: 0.75rem;
  z-index: 2;
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--on-document-scrim);
  color: var(--on-document-text);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
}

.viewer-close:hover { background: var(--on-document-scrim-strong); }

.viewer-body {
  position: relative;
  height: 100%;
  overflow: hidden;      /* clips the track to one page at a time (CF-VIEW-013) */
  touch-action: pan-y;   /* a horizontal drag is the page-swipe, not a scroll */
}

.viewer-track {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Both pages stack in the same place and move independently by their own
   `transform` (`viewer.js`'s `_prepareNeighbour`/`_playSlide`/`_playSnapBack`)
   rather than sharing one track transform — a slide needs the outgoing and
   incoming picture on screen at once mid-transition, on whichever side each
   is entering or leaving from, which a single shared offset cannot express
   for both directions at once (see `viewer.js`'s own top comment).
   `transform` rather than `left`: the property the browser can animate on
   its own thread, so the slide stays smooth even while the main thread is
   busy drawing the neighbour's canvas. */
.viewer-page {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 1rem;
  will-change: transform;
}

.viewer-page canvas,
.viewer-page .viewer-image {
  max-width: min(60rem, 100%);
  max-height: 100%;
  background: var(--thumb-bg);
  border-radius: 0.25rem;
  box-shadow: 0 8px 40px var(--on-document-scrim);
}

.viewer-page .viewer-image {
  object-fit: contain;
}

/* A PDF (or other browser-native content) fills the page — its own viewer
   supplies scrolling/pagination (CF-VIEW-004), so it gets the full frame
   rather than the canvas's fixed A4-ish aspect ratio. */
.viewer-page .viewer-frame {
  width: min(60rem, 100%);
  height: 100%;
  border: none;
  background: var(--thumb-bg);
  border-radius: 0.25rem;
  box-shadow: 0 8px 40px var(--on-document-scrim);
}

.viewer-page .viewer-download {
  display: flex;
  align-items: center;
  justify-content: center;
  width: min(60rem, 100%);
  aspect-ratio: 1 / 1.414;
  max-height: 100%;
  background: var(--placeholder-bg);
  border: 1px solid var(--placeholder-line);
  border-radius: 0.25rem;
  color: var(--on-document-text);
  font-weight: 600;
  text-decoration: none;
}

.viewer-page .viewer-download:hover {
  background: var(--placeholder-line);
}

.viewer-page .viewer-download[hidden] { display: none; }

/* Hover-revealed arrows (CF-VIEW-012): a mouse has no swipe of its own, so
   the affordance a touch user gets from the image itself is given a visible
   control here instead, appearing only near the edge it belongs to rather
   than sitting on screen permanently. `@media (hover: none)` keeps this off
   a touch device entirely — there is no hover there to reveal it with, and a
   half-visible arrow that never quite appears is worse than none (the same
   reasoning as the label pill's touch menu, CF-LBL-024). */
.viewer-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 2.5rem;
  height: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--on-document-scrim);
  color: var(--on-document-text);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s;
}

@media (hover: none) {
  .viewer-nav { display: none; }
}

.viewer-nav-prev { left: 0.75rem; }
.viewer-nav-next { right: 0.75rem; }

.viewer-nav:disabled { display: none; }

.viewer-nav:hover { background: var(--on-document-scrim-strong); }

/* Revealed by proximity to the edge it belongs to, not the whole viewer —
   hovering the middle of the image should not raise both arrows at once. */
.viewer-body:hover .viewer-nav-prev,
.viewer-nav-prev:focus-visible {
  opacity: 1;
}

.viewer-body:hover .viewer-nav-next,
.viewer-nav-next:focus-visible {
  opacity: 1;
}

/* Document-walker buttons: always jump to the next/previous document,
   skipping any remaining pages of a multi-page document — a distinct action
   from the page-walker arrows above, which is why the two are placed apart
   (top corners here, vertically centred there) rather than sharing one
   ambiguous control. Same hover-reveal idiom as the page-walkers on a
   hover-capable device (CF-VIEW-012's reasoning applies equally: a control
   that only sits there is competing with the page-walkers' identical shape),
   but a touch device has no hover to reveal a document-walker with, and
   unlike a page step — which touch already reaches via the drag/swipe
   falling through to the next document — jumping straight to another
   document has no other affordance on touch, so it stays permanently
   visible there instead of absent. */
.viewer-docnav {
  position: absolute;
  /* Same reasoning as `.viewer-close`'s `top`: without the status bar's own
     height added in, these sat half-hidden behind it on a phone. */
  top: calc(0.75rem + env(safe-area-inset-top, 0px));
  z-index: 2;
  height: 2rem;
  padding: 0 0.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--on-document-scrim);
  color: var(--on-document-text);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s;
}

.viewer-docnav-prev { left: 0.75rem; }
.viewer-docnav-next { right: 0.75rem; }

.viewer-docnav:disabled { display: none; }

.viewer-docnav:hover { background: var(--on-document-scrim-strong); }

.viewer-body:hover .viewer-docnav,
.viewer-docnav:focus-visible {
  opacity: 1;
}

@media (hover: none) {
  .viewer-docnav:not(:disabled) { opacity: 1; }
}

/* The info toggle and panel (CF-VIEW-009): the same bottom-overlay shape the
   results grid draws its label pills in (`.thumb-labels`), so date and
   labels read as "the same information, shown bigger" rather than a new
   idiom. */
.viewer-info-toggle {
  position: absolute;
  right: 0.75rem;
  bottom: 0.75rem;
  z-index: 2;
  width: 1.9rem;
  height: 1.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: var(--on-document-scrim);
  color: var(--on-document-text);
  font: italic 700 0.85rem Georgia, serif;
  cursor: pointer;
}

/* `[hidden]` (CF-DOC-011: hidden for a standalone document, `viewer.js`'s
   `open`) would otherwise lose to this rule's own `display: flex` — same
   specificity, later in the cascade, the same bug already fixed for
   `.bar`/`.results-pane`/`.settings-panel` (TASKS.md, "Mobile layout"). */
.viewer-info-toggle[hidden] { display: none; }

.viewer-info-toggle:hover,
.viewer-info-toggle[aria-expanded="true"] { background: var(--on-document-scrim-strong); }

.viewer-info {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  padding: 0.75rem 3rem 0.75rem 1rem;
  background: linear-gradient(to top, var(--on-document-scrim-strong), transparent);
  border-radius: 0 0 0.25rem 0.25rem;
}

.viewer-info[hidden] { display: none; }

.viewer-labels {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  flex: 1;
  min-width: 0;
}

.viewer-date {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--on-document-text);
  white-space: nowrap;
}
