/* justelesrecospsy - one small stylesheet, no external fonts, no framework.

   The palette is deliberately identical to ../justelesRCP/src/style.css: the two
   sites are siblings and should look it. Only the token values are shared; the
   layout below is this site's own, because a corpus search over many PDFs has
   nothing structurally in common with one drug's leaflet. Keeping the tokens in
   sync by hand is the tradeoff for not shipping a shared stylesheet between two
   independently deployed projects. */
:root {
  color-scheme: light;
  --bg: #ffffff;
  --fg: #1a1c20;
  --muted: #5b616e;
  --accent: #8a5cf0;
  /* Same hue as --accent as an "R G B" triple, so rgb(var(--accent-rgb) / a) can
     derive translucent variants (the tour glow, the highlight fills) from one
     source colour. CSS cannot extract channels from a hex var, so this must be
     kept in step with --accent by hand. */
  --accent-rgb: 138 92 240;
  --line: #e4e7ec;
  --card: #f7f8fa;
  --d-bg: #14161a;
  --d-fg: #e7e9ee;
  --d-muted: #9aa2b1;
  --d-accent: #b48bff;
  --d-accent-rgb: 180 139 255;
  --d-line: #2a2e37;
  --d-card: #1c1f26;
  /* The matched passage is FRAMED, not filled. A translucent wash over the words
     was the first version and it cost exactly what it saved: the text under it
     sat behind a purple veil, on a scanned page at that, and reading the passage
     you came for was harder than reading the rest of the page. The frame sits in
     the margin around the block instead, so nothing is drawn over a single glyph
     and the eye still lands on it from across the page. The halo is what does
     that landing, and it is outside the frame for the same reason. */
  --hit-best-edge: rgb(var(--accent-rgb) / 0.95);
  --hit-halo: rgb(var(--accent-rgb) / 0.18);
  /* The fill for the text highlight. Weak on purpose, and applied with
     mix-blend-mode: multiply, so it tints the PAPER and leaves the glyphs at full
     contrast instead of putting a veil over them: multiplying white paper by the
     fill gives the fill, and multiplying black ink by it gives black. That is what
     makes a fill usable here where the flat translucent one described above was
     not. */
  --hit-fill: rgb(var(--accent-rgb) / 0.28);
  /* These two are ONE decision in two places and must move together, so they are
     tokens rather than a pair of dark-mode rules that a later edit can separate.
     pdf.js renders the PDF as published, which means WHITE paper whatever the site
     theme is, so the blend mode that suits the highlight depends entirely on whether
     the canvas above has been inverted. See the rules further down. */
  --page-invert: none;
  --hit-blend: multiply;
  --maxw: 60rem;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: var(--d-bg); --fg: var(--d-fg); --muted: var(--d-muted);
    --accent: var(--d-accent); --accent-rgb: var(--d-accent-rgb);
    --line: var(--d-line); --card: var(--d-card);
    --page-invert: invert(1) hue-rotate(180deg);
    --hit-blend: screen;
    --hit-fill: rgb(var(--d-accent-rgb) / 0.40);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: var(--d-bg); --fg: var(--d-fg); --muted: var(--d-muted);
  --accent: var(--d-accent); --accent-rgb: var(--d-accent-rgb);
  --line: var(--d-line); --card: var(--d-card);
  --page-invert: invert(1) hue-rotate(180deg);
  --hit-blend: screen;
  /* A heavier wash than the 0.28 light mode uses. Screen onto black paper starts
     from nothing and adds, where multiply onto white starts from full brightness and
     takes away, so the same alpha reads weaker on the inverted page. Matched by eye
     against the light crop rather than by arithmetic, since the two blends are not
     comparable numerically. */
  --hit-fill: rgb(var(--d-accent-rgb) / 0.40);
}
* { box-sizing: border-box; }
/* overscroll-behavior-x stops a horizontal trackpad swipe from pulling the page
   sideways into empty space. Every page here is one column that never needs to
   scroll horizontally, so there is nothing to the right to reach, but a browser
   will still rubber-band toward it on a two-finger swipe and the reader sees the
   layout slide off and blank paper appear.

   `none` rather than `contain`, and on html rather than body, because the aim is
   to stop the gesture at the page rather than to stop it propagating to an
   ancestor. It does NOT set overflow, so it creates no scroll container and the
   sticky header keeps working, which `overflow-x: hidden` on body would have
   risked. Nested scrollers opt back in: .page-scroll in the viewer sets its own
   overscroll-behavior-x: auto, since panning a zoomed PDF sideways is real
   scrolling over real content. */
html { -webkit-text-size-adjust: 100%; overscroll-behavior-x: none; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
a { color: var(--accent); }
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 1rem; }

/* --- header ------------------------------------------------------------- */
header.top {
  position: sticky; top: 0; z-index: 10;
  background: var(--bg); border-bottom: 1px solid var(--line);
}
/* Wraps, because it has to: the wordmark plus four controls measure about 500 px
   and a phone is 360. Without `flex-wrap` the row simply ran off the side of the
   screen, taking the whole page with it (the header is 100% wide, so its overflow
   is the document's), which reads as a broken layout rather than a wide header.
   The `.spacer` below has `flex: 1` and a zero base, so it eats the free space of
   the first line and the controls drop to a second line under the wordmark rather
   than crowding it. `row-gap` is smaller than the column gap: two lines a full
   .75rem apart made the header taller than the search box under it. */
header.top .wrap {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .75rem; row-gap: .4rem; padding: .6rem 1rem;
}
.brand { font-weight: 700; text-decoration: none; color: var(--fg); white-space: nowrap; }
.brand span { color: var(--accent); }
.spacer { flex: 1; }
button.plain {
  background: var(--card); color: var(--fg); border: 1px solid var(--line);
  border-radius: .4rem; padding: .3rem .6rem; font: inherit; font-size: .85rem;
  cursor: pointer;
}
button.plain:hover { border-color: var(--accent); }
button.plain[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }

/* The link between the two pages, styled as the buttons beside it are: it sits in
   the same row and does the same kind of thing, and a link that looked like a link
   there would read as the odd one out. Underlined on hover all the same, since it
   navigates and they do not. */
a.plain.nav {
  background: var(--card); color: var(--fg); border: 1px solid var(--line);
  border-radius: .4rem; padding: .3rem .6rem; font-size: .85rem;
  text-decoration: none; white-space: nowrap;
}
a.plain.nav:hover { border-color: var(--accent); text-decoration: underline; }
/* The page you are already on, marked the way a pressed button is marked, because
   the two nav links now sit side by side on every page and otherwise nothing in
   the bar would say which of the two you are looking at. */
a.plain.nav[aria-current="page"] { border-color: var(--accent); color: var(--accent); }

/* --- search ------------------------------------------------------------- */
.searchbox { display: flex; gap: .5rem; margin: 1.25rem 0 .5rem; align-items: flex-start; }
.searchbox textarea {
  /* `min-width: 0` undoes the `min-width: auto` a flex item gets by default, which
     for a text input is its intrinsic size (roughly 20 characters). Without it the
     input refuses to shrink past that, and on a 320 px screen the Search button
     beside it is pushed off the edge of the page. */
  flex: 1; min-width: 0; font: inherit; padding: .6rem .75rem;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: .5rem;
  /* The height is set from the content by app.js, so the browser's own resize
     grip would fight it: drag it taller and the next keystroke snaps it back.
     Vertical overflow is hidden for the same reason, since the box is never
     shorter than its text. */
  resize: none; overflow-y: hidden; line-height: 1.4;
}
.searchbox textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The button is as tall as the two rows beside it, rather than floating at the top
   of a box that grows away from it. */
.searchbox button { align-self: stretch; }
.status { color: var(--muted); font-size: .9rem; min-height: 1.4em; }
.status.error { color: #c0392b; }
/* The sentence that points at the other page, on both pages. Muted: it is an aside
   next to the thing the reader came for, whether that is the box or the list. */
.hint { color: var(--muted); font-size: .9rem; margin: .4rem 0 1rem; }
.hint[hidden] { display: none; }

/* --- filters ------------------------------------------------------------ */
/* Facets are built from the data, so this grid holds however many columns of
   MANIFEST.tsv are actually curated. An uncurated column renders nothing at all
   rather than an empty control, which is why the filter panel grows over time
   instead of needing a rewrite. */
/* Two hosts for the same panel, on purpose. On the search page the knobs live
   inside the "recherche avancée" disclosure, because a question is the point
   there and metadata is the exception. On the corpus listing filtering IS the
   page, so the same block sits open and needs a frame of its own. One class, so
   the two pages cannot drift apart. */
section.filters { border: 1px solid var(--line); border-radius: .5rem; margin: .5rem 0 1rem; }
.filters-title { margin: 0; padding: .5rem .75rem .1rem; font-size: .9rem; }
.facets { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
          gap: .75rem; padding: .25rem .75rem .75rem; }
.facet-label { display: block; font-size: .78rem; text-transform: uppercase;
               letter-spacing: .04em; color: var(--muted); margin-bottom: .25rem; }
/* :not(checkbox), because the box in a choice below wants none of this. */
.facet select, .facet input:not([type="checkbox"]), .name-filter {
  width: 100%; font: inherit; font-size: .9rem; padding: .3rem .4rem;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: .35rem;
}

/* A facet's values as a list of checkboxes rather than a <select multiple>.
   Choosing two themes had to stop being a ctrl-click secret, and a native
   multiple-select shows its state only while it has room to: at twenty values it
   either eats the page or hides the selection inside its own scrollbar. The list
   of boxes says what is selected at a glance, and scrolls internally so that one
   long facet (issuer has dozens of values) cannot push the others off screen. */
.choices { display: grid; gap: .1rem; max-height: 13rem; overflow-y: auto;
           background: var(--bg); border: 1px solid var(--line); border-radius: .35rem;
           padding: .3rem .4rem; }

/* The boxes live in a popup now, opened by a button that says what the facet is
   currently doing. Six columns of scrolling checkboxes took the room of six open
   menus while showing four options each, and a reader looking for one topic out of
   thirty was reading rather than choosing. The button is the label as well as the
   control: "Thème · 2" says more than a caption above a list ever did. */
.facet-menu { position: relative; }
.facet-btn { width: 100%; text-align: left; font-size: .9rem;
             padding: .35rem .5rem; border: 1px solid var(--line);
             border-radius: .35rem; background: var(--bg); }
.facet-btn::after { content: "▾"; float: right; color: var(--muted); }
/* A facet that is narrowing the corpus says so on its own button: the panel can be
   scrolled past, and a filter nobody remembers setting is the one that makes the
   corpus look empty. */
.facet-btn.narrowed { border-color: var(--accent); color: var(--accent); }
.facet-btn[aria-expanded="true"] { border-color: var(--accent); }
.facet-pop { position: absolute; z-index: 20; top: calc(100% + .25rem); left: 0;
             min-width: 100%; width: max-content; max-width: min(22rem, 90vw);
             background: var(--card); border: 1px solid var(--line);
             border-radius: .4rem; padding: .4rem; box-shadow: 0 6px 20px rgb(0 0 0 / .18); }
.facet-pop .choices { border: none; background: none; padding: .1rem; }
.facet-search { margin-bottom: .3rem; }
.facet-empty { margin: .2rem .1rem 0; font-size: .85rem; color: var(--muted); }
.choice { display: flex; gap: .4rem; align-items: baseline; font-size: .9rem;
          line-height: 1.25; cursor: pointer; }
.choice input[type="checkbox"] { flex: none; margin: 0; accent-color: var(--accent); }
/* `display: flex` above beats the [hidden] attribute's own `display: none`, so the
   popup's search box would hide nothing without this line. */
.choice[hidden] { display: none; }
/* Issuer values are long and unbreakable ("CNEDiMTS"); wrapping inside the box
   beats a horizontal scrollbar under every group. */
.choice span { min-width: 0; overflow-wrap: anywhere; }

/* The document-name box sits in the same panel as the facets rather than above
   it: it filters the same list by the same click, and two separate controls made
   the page look as though the name search and the metadata search were different
   searches. */
.name-filter { margin-top: .3rem; }

/* The year range: one track with a handle at each end.

   There is still no two-handle input element, so this is two native sliders laid on
   top of each other rather than a widget written from scratch: the keyboard support,
   the focus ring and the touch targets are the browser's, and each handle can be
   tabbed to and nudged with the arrow keys exactly as before. What is drawn is the
   wrapper's own track plus the lit segment between the two values, so the reader
   sees a single range rather than two controls that happen to share a meaning.

   Each input is transparent and lets pointer events through; only its thumb takes
   them, which is what allows the lower handle to be grabbed through the upper
   slider's track. */
.facet .range { display: grid; gap: .15rem; }
.facet .range-track {
  position: relative; height: 1.5rem; display: grid; align-items: center;
}
/* The unlit rail, drawn once behind both inputs. */
.facet .range-track::before {
  content: ""; position: absolute; left: 0; right: 0; height: .3rem;
  border-radius: .3rem; background: var(--line);
}
/* The selected span, positioned by app.js from the two values. */
.facet .range-fill {
  position: absolute; height: .3rem; border-radius: .3rem;
  background: var(--accent); pointer-events: none;
}
.facet .range-track input[type="range"] {
  position: absolute; left: 0; width: 100%; margin: 0; padding: 0;
  height: 1.5rem; background: none; border: none; appearance: none;
  pointer-events: none;   /* the track is the wrapper's; only the thumb is grabbable */
}
.facet .range-track input[type="range"]::-webkit-slider-runnable-track {
  background: none; border: none; height: 1.5rem;
}
.facet .range-track input[type="range"]::-moz-range-track { background: none; border: none; }
.facet .range-track input[type="range"]::-webkit-slider-thumb {
  appearance: none; pointer-events: auto; cursor: pointer;
  width: .95rem; height: .95rem; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--card);
  box-shadow: 0 0 0 1px var(--accent);
}
.facet .range-track input[type="range"]::-moz-range-thumb {
  pointer-events: auto; cursor: pointer;
  width: .95rem; height: .95rem; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--card);
  box-shadow: 0 0 0 1px var(--accent);
}
/* Keyboard focus has to stay visible: the input itself is invisible, so the ring
   goes on the thumb, which is the part a reader is looking at. */
.facet .range-track input[type="range"]:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--fg); outline-offset: 2px;
}
.facet .range-track input[type="range"]:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--fg); outline-offset: 2px;
}
.range-readout {
  font-size: .9rem; font-variant-numeric: tabular-nums; color: var(--fg);
  padding: .1rem 0 .15rem;
}

/* "Recherche avancée": one disclosure holding everything that is not the box.
   It used to be two stacked ones, the query algebra and the filters, which read
   as two settings menus for a page whose only real control is the text field.
   Nothing inside is needed to search, so nothing inside competes with the box
   for attention until the reader opens it. */
details.advanced { border: 1px solid var(--line); border-radius: .5rem; margin: .5rem 0 1rem; }
details.advanced > summary { padding: .5rem .75rem; cursor: pointer; font-size: .9rem; }
/* Closed, the summary is the only thing left on screen that can say the corpus is
   being narrowed, so it says it. Without this a reader who set a filter, collapsed
   the panel and then wondered where two thirds of the answers went would have
   nothing on the page to look at. */
details.advanced.narrowed > summary { color: var(--accent); }
details.advanced .filters-title { border-top: 1px solid var(--line);
                                  margin-top: .25rem; padding-top: .5rem; }

/* The query-algebra hint, no longer collapsible: it is already inside a
   disclosure, and a second click to read what the reader has just asked to see
   would be a click for nothing. */
.syntax { margin: 0; padding: 0 .75rem; font-size: .85rem; color: var(--muted); }
.syntax p { margin: .4rem 0; }
.syntax ul { margin: .4rem 0 .6rem; padding-left: 1.1rem; }
.syntax li { margin: .3rem 0; }
.syntax code { font-size: .95em; padding: .05rem .3rem; border-radius: .25rem;
               background: var(--card); color: var(--fg); }

/* --- results ------------------------------------------------------------ */
/* The passage/document switch. A pair of buttons rather than a <select>, because
   there are exactly two of them and the current one has to be readable without
   opening anything. aria-pressed carries the state.
   The pressed style itself comes from the button.plain[aria-pressed] rule above,
   which already exists for the theme button: one definition, two users. */
/* The row above the results: how they are stacked, and how they were ranked. Wraps
   on a phone so the checkbox drops under the view buttons instead of squeezing them. */
.result-controls { display: flex; flex-wrap: wrap; align-items: center;
                   gap: .4rem .9rem; margin: 0 0 .7rem; }
/* display:flex above would otherwise beat the UA sheet's [hidden]. */
.result-controls[hidden] { display: none; }
.view-toggle { display: flex; gap: .4rem; }
.view-toggle[hidden] { display: none; }
/* The label is the hit area, so the words are clickable and not just the box. */
.rescore-toggle { display: inline-flex; align-items: center; gap: .35rem;
                  font-size: .85rem; color: var(--muted); cursor: pointer;
                  user-select: none; }
.rescore-toggle input { accent-color: rgb(var(--accent-rgb)); cursor: pointer; }

ol.results { list-style: none; margin: 0; padding: 0; }
/* The passages nested under a document in the grouped view. Indented by a rule in
   the accent colour rather than by bullets, so the eye can follow one document's
   passages down and skip to the next document's title. */
ol.results .passages { list-style: none; margin: .45rem 0 0; padding: 0 0 0 .7rem;
                       border-left: 2px solid rgb(var(--accent-rgb) / .35); }
ol.results .passages > li { margin: 0 0 .35rem; font-size: .9rem; }
ol.results .passages > li:last-child { margin-bottom: 0; }
.passage-page { color: var(--muted); font-size: .8rem; white-space: nowrap;
                font-variant-numeric: tabular-nums; }
ol.results > li {
  border: 1px solid var(--line); border-radius: .5rem;
  padding: .7rem .85rem; margin-bottom: .6rem; background: var(--card);
}
.hit-head { display: flex; gap: .5rem; align-items: baseline; flex-wrap: wrap; }
.hit-title { font-weight: 600; text-decoration: none; color: var(--fg); }
.hit-title:hover { color: var(--accent); }
.hit-meta { color: var(--muted); font-size: .82rem; }
/* The publisher link. Muted like the line it sits on, dotted rather than solid so it
   reads as a claim to check and not as another of the site's own files, and never
   broken across lines: "Source" on one line and "(IA)" on the next would lose the
   caveat that is the reason the label carries it. */
.src-ai { color: var(--muted); text-decoration: underline dotted; white-space: nowrap; }
.src-ai:hover { color: var(--accent); }
.hit-score { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums;
             font-size: .8rem; }
/* `pre-line` rather than `normal`: the chunker keeps the paragraph and list breaks
   of the original (chunk.py, format 13), and a list of recommendations run together
   into one paragraph is unreadable next to the page it came from. Runs of spaces
   still collapse, which is what separates this from `pre-wrap`: a PDF pads its
   columns with spaces and those are layout, not text. */
.hit-text { font-size: .93rem; color: var(--fg); text-decoration: none; cursor: pointer;
  white-space: pre-line; }
.hit-text.block { display: block; margin: .4rem 0 0; }
.hit-text:hover { color: var(--accent); }
/* The passage is a button so that clicking it opens the whole chunk in place, which
   means undoing everything a button looks like: the row must read as text. Left
   aligned and full width in the block form, so the hit area is the paragraph the
   reader is already looking at rather than a box around it. */
button.hit-text { font-family: inherit; text-align: left; background: none;
                  border: 0; padding: 0; line-height: inherit; }
button.hit-text.block { width: 100%; }
/* Opened, the passage grows from a snippet into the whole chunk, which is a
   paragraph or more: as a colour, the open state meant reading that paragraph in
   purple. The viewer had already answered the same question for the highlighted
   region of a PDF page, by framing it instead of washing it, so the open passage
   borrows the same two tokens and the words stay in the reading colour.
   Three rings rather than an outline plus a shadow, because the gap between the
   text and the frame is painted by neither: the first ring repaints the row's own
   background to make the gap, the second is the frame, the third is the halo. None
   of them affects layout, and 10px stays inside the row's own .85rem padding. */
button.hit-text[aria-expanded="true"] {
  border-radius: 3px;
  box-shadow: 0 0 0 4px var(--card),
              0 0 0 6px var(--hit-best-edge),
              0 0 0 10px var(--hit-halo);
}
/* The hover colour is for picking a passage out of a list of snippets. Once one is
   open it is a block of prose, and turning all of it purple on the way past is the
   thing the frame exists to avoid. */
button.hit-text[aria-expanded="true"]:hover { color: var(--fg); }
/* A passage with nothing folded away gets no cursor and no hover, because clicking it
   would do nothing. */
.hit-text.plain-text { cursor: default; }
.hit-text.plain-text:hover { color: var(--fg); }
.hit-text mark { background: rgb(var(--accent-rgb) / .22); color: inherit; padding: 0 .1em; }
.hit-extra { margin: .35rem 0 0; font-size: .82rem; color: var(--muted); }
.hit-extra a { color: var(--muted); }
.hit-extra a:hover { color: var(--accent); }
.empty { color: var(--muted); padding: 1.5rem 0; }

/* --- viewer ------------------------------------------------------------- */
.viewer-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
              margin: .75rem 0; font-size: .9rem; }
/* Under the page and centred, see view.html. inline-flex keeps the three controls on
   one line whatever the width. */
.pager { display: flex; justify-content: center; align-items: center; gap: .5rem;
         margin: .75rem 0 .25rem; }
/* These five rules used to be style="" attributes in the markup. They are here
   because the production CSP is `style-src 'self'` with no 'unsafe-inline', which
   blocks a style ATTRIBUTE parsed from HTML (setting el.style from script is not
   blocked, which is why the tour bubble and the highlight boxes were unaffected).
   A blocked attribute fails quietly: the page still renders, slightly wrong, and
   the console fills with violations. */
.tagline { font-size: 1.15rem; font-weight: 600; margin: 1.1rem 0 0; }
.filter-body { padding: 0 .75rem .75rem; }
.doc-title { font-size: 1.05rem; font-weight: 600; margin: .2rem 0; }
/* Tabular figures so "9 / 10" does not shift width as the page number changes. */
#pageno { font-variant-numeric: tabular-nums; }
.plain.bare { text-decoration: none; }
/* One document, three stacked layers and a scroller, in four boxes:

   .page-frame   stays put and owns the overlaid controls
     .page-scroll  clips and scrolls the page when it is zoomed past the frame
       .page-stage   the page itself, at its rendered CSS size
         canvas / .hit-layer / .text-layer, all the same box

   The frame exists so the highlight toggle can sit over a corner of the document
   without scrolling away with it. viewer.js measures .page-scroll to decide how
   wide to render, so the frame must not be what carries the overflow. */
.page-frame { position: relative; margin: 0 auto 2rem; }
/* Expanded is one state, not a rung on a zoom ladder (see viewer.js): the frame
   becomes an overlay filling the viewport and the page is re-rendered to its width.
   Fixed rather than native-fullscreen-only, because element fullscreen does not exist
   everywhere and the state has to look the same where it does not. Flex so the
   scroller takes the whole height and the page can be scrolled through inside it. */
.page-frame.expanded {
  position: fixed; inset: 0; z-index: 50; margin: 0;
  display: flex; background: var(--bg);
}
.page-frame.expanded .page-scroll { flex: 1; height: 100%; padding: 0 0 2rem; }
.page-scroll { overflow: auto; cursor: zoom-in; overscroll-behavior-x: auto; }
.page-frame.expanded .page-scroll { cursor: zoom-out; }
.page-stage { position: relative; margin: 0 auto; width: fit-content;
              box-shadow: 0 1px 6px rgb(0 0 0 / .18); }
/* No max-width: the canvas is rendered at exactly the width it should occupy, and a
   max-width here would shrink a zoomed page back into the frame instead of letting
   it scroll. */
/* The page is inverted in dark mode, which is the only way a white A4 scan is
   readable next to a dark interface: at 2am the page was the one bright rectangle
   on the screen. invert(1) alone would also complement every hue, turning a red
   warning box cyan, so hue-rotate(180deg) turns them back: the pair flips lightness
   and leaves hue roughly where the publisher put it. Greys are unaffected by the
   rotation, so white paper goes black and black ink goes white, which is the point.

   The cost, and it is real: a PHOTOGRAPH inside a document becomes a negative, and
   nothing in CSS can tell a photo from a flowchart. Most figures in this corpus are
   line diagrams and algorithm trees, which inverts cleanly. If the negatives turn
   out to matter more than the glare, this belongs behind a toggle beside the
   highlight one rather than tied to the theme.

   A filter here gives the canvas its own stacking context. The highlight layer is a
   SIBLING, painted after it, so it still blends against these pixels: that is what
   --hit-blend depends on, and why the two tokens travel together. */
.page-stage canvas { display: block; height: auto; filter: var(--page-invert); }
/* The highlight layer sits over the canvas in the SAME coordinate space, scaled
   with it, so a box computed in PDF points lands on the right words at any zoom.
   pointer-events: none keeps the text layer above it reachable. */
.hit-layer { position: absolute; inset: 0; pointer-events: none; }
/* Explicit rather than relying on the UA sheet: .hit-layer sets position, and a
   future display rule here would silently beat [hidden] and leave the highlights on
   screen with the button saying they are off. */
.hit-layer[hidden] { display: none; }
.hit-layer .box { position: absolute; border-radius: 3px; }
/* The highlight itself: a fill over the words, one box per LINE, the way a marker
   pen leaves ink on the lines it crossed. The point is that the reader can see
   exactly which text the chunk covers, which a frame around a block cannot show:
   a frame around eleven lines says nothing about whether the last line is included
   in full or only up to its third word.

   The blend mode is not a style choice, it follows the page under it, which is why
   it is --hit-blend rather than a literal here. A flat rgba fill lightens the glyphs
   under it towards the fill colour, which on a scanned page turns grey ink into pale
   purple: that is the veil this project tried once and removed. A blend that can only
   move the paper one way avoids it.

   On an UNinverted page (light mode) the paper is white and the ink black, so the
   fill must only ever darken: multiply leaves white paper tinted and black ink black.
   On an INVERTED page (dark mode) the paper is black and the ink white, so it must
   only ever lighten: screen lifts the black paper towards the accent and leaves the
   white glyphs white. Using multiply on the inverted page was the bug this replaces,
   and it is worth naming because it looks like nothing rather than like an error: a
   dark page multiplied by anything stays dark, so the highlight simply was not there.

   No outline and no halo. With a fill on every line the passage already has a shape
   the eye lands on, and a frame around the block on top of it would be drawing the
   same thing twice, less precisely. */
.hit-layer .box.best {
  background: var(--hit-fill);
  mix-blend-mode: var(--hit-blend);
}

/* pdf.js's text layer: invisible text positioned over the rendered page, so the
   document can be selected and copied like a web page while still LOOKING like the
   PDF it is. Transparent rather than absent, so what the reader selects is the real
   text and what they see is the real page.

   The rules are pdf.js's own contract (viewer/text_layer_builder.css), written out
   here rather than vendored as a second stylesheet: these six declarations are all
   of it that a read-only viewer needs, and the production CSP serves one stylesheet
   from this origin. --scale-factor is set by viewer.js; without it every span is
   positioned at font-size zero. */
.text-layer { position: absolute; inset: 0; overflow: hidden; line-height: 1;
              text-size-adjust: none; forced-color-adjust: none; opacity: 1;
              text-align: initial; }
.text-layer span { position: absolute; white-space: pre; transform-origin: 0 0;
                   color: transparent; cursor: text; }
/* Selection has to be visible even though the text is not. */
.text-layer ::selection { background: rgb(var(--accent-rgb) / .35); }
.text-layer br { user-select: none; }

/* The highlight toggle, over the bottom-right corner of the page. Over it rather
   than in the bar because it is about what is on the page, and a reader comparing
   the page with and without the colours should not have to look away from it to
   flip them. */
.plain.overlay {
  position: absolute; right: .6rem; bottom: .6rem; z-index: 2;
  background: var(--card); box-shadow: 0 1px 6px rgb(0 0 0 / .25);
  font-size: .82rem; padding: .3rem .55rem;
}

/* "PDF complet (N pages)", opening onto the two things that can be done with it.
   A <details> rather than a <select> or a hand-rolled menu: it is keyboard
   operable, it closes on Escape, and it needs no JavaScript to work. */
.pdf-menu, .doc-search { position: relative; }
.pdf-menu > summary, .doc-search > summary {
  list-style: none; cursor: pointer; display: inline-block;
  font: inherit; padding: .35rem .6rem; border: 1px solid var(--line);
  border-radius: .4rem; background: var(--bg); color: var(--fg);
}
.pdf-menu > summary:hover, .doc-search > summary:hover { border-color: var(--accent); }
/* A document the site may show but not hand over. It keeps the shape of the control it
   replaces, so the row does not jump between a document that offers its file and one
   that does not, and it is dimmed and not clickable because there is nothing behind it.
   `help` rather than `not-allowed`: the reason is in the title attribute, and the
   pointer is what invites the reader to go and read it. */
.pdf-menu.restricted {
  display: inline-block; padding: .35rem .6rem; border: 1px dashed var(--line);
  border-radius: .4rem; color: var(--muted); cursor: help;
}
.restricted-note { color: var(--muted); }
.pdf-menu > summary::-webkit-details-marker,
.doc-search > summary::-webkit-details-marker { display: none; }
.pdf-menu[open] > summary, .doc-search[open] > summary {
  border-color: var(--accent); color: var(--accent);
}
.pdf-menu-items, .doc-search-panel {
  position: absolute; right: 0; top: calc(100% + .25rem); z-index: 3;
  display: grid; min-width: max-content;
  background: var(--card); border: 1px solid var(--line); border-radius: .4rem;
  box-shadow: 0 2px 10px rgb(0 0 0 / .2); padding: .25rem;
}
/* Wider than the PDF menu because it holds a question rather than two verbs, and
   capped so a narrow phone screen does not get a panel wider than the window. */
.doc-search-panel { gap: .5rem; padding: .6rem; width: min(22rem, 80vw); }
.doc-search-panel input[type="search"] {
  font: inherit; padding: .4rem .5rem; border: 1px solid var(--line);
  border-radius: .35rem; background: var(--bg); color: var(--fg); width: 100%;
}
.doc-search-panel fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .2rem; }
.doc-search-panel legend { padding: 0; color: var(--muted); font-size: .82rem; }
.doc-search-panel label { display: flex; gap: .4rem; align-items: baseline;
                          font-size: .88rem; cursor: pointer; }
.doc-search-panel label[hidden] { display: none; }
.doc-search-panel button { justify-self: start; }
.pdf-menu-items a { padding: .35rem .6rem; border-radius: .3rem; text-decoration: none;
                    color: var(--fg); }
.pdf-menu-items a:hover { background: rgb(var(--accent-rgb) / .12); color: var(--accent); }

footer.bot { border-top: 1px solid var(--line); margin-top: 2rem; padding: 1rem 0 2rem;
             color: var(--muted); font-size: .85rem; }
.bot p { margin: 0; }
.bot p + p { margin-top: .4rem; }
/* The corpus count is only written on the search page, and the viewer would otherwise
   open the footer with a blank line where it would have gone. */
.bot p:empty { display: none; }
/* Footer links stay in the muted colour so the credit line reads as one sentence
   rather than as a row of calls to action; the underline still marks them. */
.bot a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.bot a:hover { color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .pulse { animation: pulse 1.4s ease-out 2; }
  @keyframes pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0); }
    40% { box-shadow: 0 0 0 .4rem rgb(var(--accent-rgb) / .35); }
  }
}

/* The release-notes popup (src/changelog.js). Fixed overlay plus a card that is at
   most as tall as the viewport, with only its body scrolling: a release with thirty
   bullets must not push its own close button off the screen, which is what a card
   that grows with its content does on a phone.

   No style attributes anywhere in changelog.js, so everything it needs is here:
   docker/Caddyfile serves style-src 'self', which blocks a style="..." attribute
   outright while leaving el.style.x from script alone. */
.cl-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: 1rem; background: rgb(0 0 0 / .45);
}
/* The page behind stops scrolling while the popup is open. Without it, scrolling to
   the end of the notes carries on scrolling the result list underneath. */
body.cl-open { overflow: hidden; }
.cl-card {
  display: flex; flex-direction: column; gap: .5rem;
  width: min(42rem, 100%); max-height: min(80vh, 44rem);
  background: var(--card); color: var(--fg);
  border: 1px solid var(--line); border-radius: .6rem;
  padding: 1rem 1.1rem; box-shadow: 0 4px 24px rgb(0 0 0 / .3);
}
.cl-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.cl-title { margin: 0; font-size: 1.15rem; }
.cl-x { font-size: 1.1rem; line-height: 1; padding: .15rem .45rem; }
.cl-sub { margin: 0; color: var(--muted); font-size: .9rem; }
.cl-body { overflow-y: auto; padding-right: .25rem; }
.cl-release + .cl-release { margin-top: 1.2rem; border-top: 1px solid var(--line); padding-top: 1rem; }
.cl-version { display: flex; align-items: baseline; gap: .5rem; margin: 0 0 .3rem; font-size: 1rem; }
.cl-date { color: var(--muted); font-size: .85rem; font-weight: normal; }
.cl-cat { margin: .8rem 0 .2rem; font-size: .9rem; color: var(--accent); }
.cl-items { margin: 0; padding-left: 1.1rem; }
.cl-items li { margin: .35rem 0; }
/* The commit link is a footnote to the bullet, not part of the sentence. */
.cl-sha { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .78rem;
          color: var(--muted); text-decoration: none; }
.cl-sha:hover { color: var(--accent); text-decoration: underline; }
.cl-foot { display: flex; justify-content: flex-end; gap: .5rem; }
