/* veda-text-image-viewer — two-pane reading viewer (DESIGN section 8). */

:root {
  --status-matched: #2f7d32;
  --status-partial: #b26a00;
  --status-ambiguous: #6a4fb3;
  --status-unmatched: #777777;

  /* The verification ladder. FIVE DISCRETE STATES, NOT A RAMP: these are not
     ordered and must not be given ordered hues. The refuting colour is the only
     warm alarm on a text card, and it is there because "somebody checked and
     this is wrong" is the most informative thing the site can say about a unit —
     not because it is the bottom of a scale. */
  --verify-unverified: #8a857a;
  --verify-bracket: #2f5d8a;
  --verify-gold: #7a5c00;
  --verify-human: #2f7d32;
  --verify-refuted: #b3261e;

  --bg: #f6f4ef;
  --panel-bg: #ffffff;
  --ink: #1d1b16;
  --muted: #6b675e;
  --line: #ddd8cc;
  --accent: #2f5d8a;
  --selected-bg: #fff6da;
  /* Hover preview: a cool accent tint, deliberately weaker than the warm
     --selected-bg so selection always reads as the stronger state (issue #59). */
  --preview-bg: #edf2f8;
  --header-h: 52px;
  --footer-h: 40px;

  font-family:
    system-ui, "Hiragino Kaku Gothic ProN", "Noto Sans JP", "Yu Gothic", Meiryo,
    sans-serif;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
}

body.viewer {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ---- Header ---------------------------------------------------------- */

header.app-bar {
  height: var(--header-h);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 14px;
  background: var(--panel-bg);
  border-bottom: 1px solid var(--line);
}

header.app-bar h1 {
  font-size: 15px;
  font-weight: 700;
  margin: 0;
  white-space: nowrap;
}

header.app-bar .edition {
  font-size: 13px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Grows into any spare room, and is the FIRST thing to give it back: the 1000x
     shrink factor makes this label absorb a deficit that would otherwise be
     shared with the legend beside it (see .legend). It is the most expendable
     item in the bar — the run selector names the same run, and the label reappears
     in full in the rights footer. */
  flex: 1 1000 auto;
  min-width: 0; /* allow the label to ellipsize instead of crowding siblings */
}

/* Catalog-backed run switcher (issue #53). Utilitarian app-bar control, not a
   card; the native <select> carries the run identity (range + run_id), grouped
   into one <optgroup> per work and edition (assets/js/catalog.js,
   assets/js/work.js).

   THE INTRINSIC WIDTH IS NO LONGER SET BY THE GROUP LABELS, and this comment used
   to say it was: "a work title is longer than any range · run_id option text",
   measured at 185px before grouping and 201px after. The catalog has outgrown
   that. `YVB_TB 2.6–2.8, YVB_TB 3.1–3.7 · tb-anandasrama-v2` is 50 characters
   against the longest work title's 32, and the control's intrinsic width with the
   edition labels of issue #183 is 387px — measured 2026-09-10 by raising this
   `max-width` and reading the box back.

   WHICH IS WHY THE CAP IS THE WHOLE STORY. The control renders at exactly
   `max-width` at every breakpoint, in both languages, so nothing added to a group
   label can move it and none of the narrow-viewport caps further down had to
   change. `tests/browser/viewer-smoke.test.js` asserts that equality rather than
   a remembered pixel count, so the claim fails loudly if the catalog ever shrinks
   under it. */
.run-switch {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  font-size: 13px;
}

.run-switch select {
  font: inherit;
  font-size: 12.5px;
  max-width: 230px;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink);
  border-radius: 6px;
  padding: 3px 6px;
  cursor: pointer;
}

.run-switch select:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Catalog neighbor navigation (issue #57): compact buttons that step to the
   previous/next catalogued run, styled to match the canvas page controls. The
   visible glyph («/») is fixed, so the width never changes as a button
   enables/disables or its title is rewritten to name the target run. */
.run-switch button {
  font: inherit;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink);
  border-radius: 6px;
  padding: 3px 8px;
  line-height: 1;
  cursor: pointer;
}

.run-switch button:disabled {
  opacity: 0.4;
  cursor: default;
}

/* What the step CROSSES, marked without costing a pixel of width (issue #184).
   `data-seam` is set by assets/js/viewer.js to `sameEdition`, `otherEdition` or
   `otherWork`; over the 53 adjacent pairs of the committed catalog those are 33,
   4 and 16 seams, and every one of them looked identical before this.

   AN INSET SHADOW, not a border: the glyph's fixed width is load-bearing — the
   header must keep the run selector and the canvas controls on one row down to
   320px — and `box-shadow: inset` draws inside the existing box, so it cannot
   reflow anything. The COMMON case (another run of the same edition) is left
   unmarked on purpose: a mark on all three would distinguish none of them.

   The colour is REDUNDANT, never the only channel. The kind is spelled out in
   the button's `title` and `aria-label`, and again in `.run-seam` at the run's
   edges, so nothing here is the sole carrier of the distinction. */
.run-switch button[data-seam="otherEdition"] {
  box-shadow: inset 0 -3px 0 var(--accent);
}

.run-switch button[data-seam="otherWork"] {
  box-shadow: inset 0 -3px 0 var(--status-ambiguous);
}

.run-switch a {
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
  font-size: 12px;
}

.run-switch a:hover {
  text-decoration: underline;
}

/* Language switch (日本語 / English). Deliberately the plainest control in the
   header: text links at the run-switch back-link's size and colour, with the
   current language marked by weight rather than by a box, so it reads as a
   pointer to the other language and never competes with the run selector. It is
   the same control on both pages, so it borrows nothing page-specific. */
.lang-switch {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  font-size: 12px;
  white-space: nowrap;
}

.lang-switch a {
  color: var(--accent);
  text-decoration: none;
}

.lang-switch a:hover {
  text-decoration: underline;
}

.lang-switch .current {
  color: var(--ink);
  font-weight: 700;
}

.lang-switch .sep {
  color: var(--line);
}

/* The reading-MODE switch, 単一 / 並列, in both app bars. Deliberately the
   same shape as .lang-switch above: a marked current option and an anchor for
   the other, because both controls answer "change one thing and keep my
   place". `.unavailable` is the mode a work cannot offer -- it is rendered
   rather than hidden, so a reader can see that the choice exists and read why
   it is closed. */
.mode-switch {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  font-size: 12px;
  white-space: nowrap;
}

.mode-switch a {
  color: var(--accent);
  text-decoration: none;
}

.mode-switch a:hover {
  text-decoration: underline;
}

.mode-switch .current {
  color: var(--ink);
  font-weight: 700;
}

.mode-switch .unavailable {
  color: var(--muted);
  cursor: help;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

.mode-switch .sep {
  color: var(--line);
}

/* THE SWITCHABLE HALF OF THE CONTROL -- the mode you are NOT in -- is an anchor
   when that mode is open and a greyed span carrying the reason when it is not,
   and this rule addresses the two as ONE thing so that no width can shed one
   state and leave the other standing. That asymmetry is not hypothetical: the
   mode nav sits inside `.run-switch` in viewer.html, so `.run-switch a {
   display: none }` -- written for the "Index" back-link -- matched the anchor
   and never the span. Measured on the published site before this rule: the
   anchor computed `display: none` at 1499px in English and at every width from
   700px down in BOTH languages, while the greyed span computed `block` at all
   fourteen widths in both languages. So a three-edition work (rv-m2) showed an
   English reader "Single /" and nothing after it, and a one-edition work
   (ms-k4) still explained the mode it cannot offer: the signal was exactly
   inverted, and the only route into `compare.html` was the half that vanished.
   The declaration is a floor rather than a layout change -- these are flex
   items, so `inline` is blockified to `block` either way, and what it buys is
   specificity: `[data-mode]:not(.current)` carries one class more than any
   `html[lang="en"] .ancestor a` shed rule, so shedding the mode switch has to
   name `.mode-switch` (which takes both states down with the container) or this
   selector, and can no longer be done by a bare descendant `a`. That floor was
   checked, not assumed: with the two old `.run-switch a` shed rules put back and
   this rule left in place, the three mode-switch specs in
   tests/browser/viewer-smoke.test.js stayed green, and they failed the moment
   this rule was removed as well. */
.mode-switch [data-mode]:not(.current) {
  display: inline;
}

.canvas-switch {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
}

.canvas-switch button {
  font: inherit;
  border: 1px solid var(--line);
  background: var(--bg);
  border-radius: 6px;
  padding: 3px 9px;
  cursor: pointer;
}

.canvas-switch button:disabled {
  opacity: 0.4;
  cursor: default;
}

.highlight-switch {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  font-size: 13px;
  white-space: nowrap;
}

.highlight-switch select {
  font: inherit;
  font-size: 12.5px;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink);
  border-radius: 6px;
  padding: 3px 6px;
  cursor: pointer;
}

.legend {
  display: flex;
  gap: 7px;
  font-size: 12px;
  color: var(--muted);
  /* The one item in the bar allowed to give way, and only as a last resort.
     Everything else there is a control a reader operates -- including, since the
     viewer became bilingual, the language switch that sits after it -- while the
     legend is decorative and aria-hidden, so clipping it is what keeps a long
     translation, or a runner whose fonts measure wider than the numbers at the
     breakpoints below, from pushing a control off the edge instead.
     Order matters here: flexbox hands a deficit to shrinkable items in
     proportion to their base size, so with this and the ellipsizing edition
     label both shrinking at factor 1 the legend lost width alongside it
     (measured: 267px of a 654px legend at 1920px). The edition label carries a
     1000x shrink factor instead, which makes it absorb essentially all of any
     deficit until it has collapsed to nothing; only then does the algorithm
     redistribute to the legend. That is exactly the priority the shed ladder
     describes: ellipsize the label, then clip the legend, never a control. */
  min-width: 0;
  overflow: hidden;
}

.legend span {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}

/* The legend teaches the overlay encoding, so a swatch is a specimen of the rule
   drawn on the image (colour + line style), not a filled chip. --swatch-color is
   set by renderLegend from the model, keeping status colours model-driven. */
.legend i {
  width: 15px;
  height: 0;
  border-bottom: 3px solid var(--swatch-color, var(--status-unmatched));
  display: inline-block;
}

.legend i[data-status="partial"] {
  border-bottom-style: dashed;
}
.legend i[data-status="ambiguous"] {
  border-bottom-style: dotted;
}
.legend i[data-status="unmatched"] {
  border-bottom-style: double;
  border-bottom-width: 5px;
}

/* Non-status legend entries (issue #77 follow-up): the selection ring and
   preview ring are UI states, not model statuses, so they get small standalone
   swatches rather than a line specimen — reusing the exact box-shadow values
   the corresponding .region-overlay state uses so the legend teaches the real
   appearance, not an approximation. Selectors are qualified as `.legend i.foo`
   (not just `.foo`) to out-specify the generic `.legend i` rule above — a bare
   class selector loses that specificity contest and silently falls back to the
   line-specimen sizing/border. */
.legend i.legend-ring {
  width: 13px;
  height: 9px;
  border-bottom: none;
  border-radius: 2px;
  background: var(--bg);
}
.legend i.legend-ring.selected {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.95),
    0 0 0 2px rgba(255, 255, 255, 0.95),
    0 0 0 3px rgba(18, 18, 18, 0.92);
}
.legend i.legend-ring.preview {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 80%, transparent),
    0 0 0 1px rgba(255, 255, 255, 0.75);
}

/* Specimen of the refuting FRAME (issue #82). Same box as the two rings above --
   the treatment is a frame round a box, not a rule under a line -- and the same
   two values `.region-overlay[data-verify="human-refuted"]` draws, so the legend
   teaches the real appearance rather than an approximation. Declared AFTER
   `.legend i.legend-ring`, whose `border-bottom: none` is the same 0-2-1 as this
   selector: at equal specificity the later rule wins, and the frame would
   otherwise be missing its bottom edge. box-sizing is border-box, so the 2px
   border grows inward and the swatch stays 13x9 like its siblings.
   renderLegend sets only the class; unlike --swatch-color and --group-rule-image
   this value comes from no model field, so it belongs here and in one place. */
.legend i.legend-refuted {
  border: 2px dashed var(--verify-refuted);
}

/* Specimen of the mixed-status rule (a shared manuscript line matched to
   several verses with different statuses): same construction as
   .region-overlay[data-group-mixed]::after (a real height, not the
   height:0/border-bottom trick the single-status swatches above use — that
   trick collapses the background positioning area to zero, so a
   background-image painted through it never appears), with --group-rule-image
   set from the model's own status colours by renderLegend so this, too, stays
   model-driven. */
.legend i.legend-mixed {
  height: 3px;
  border-bottom: none;
  background-image: var(--group-rule-image);
}

/* Narrow viewports: shed app-bar items in order of importance before anything
   is forced to overlap. The run selector and canvas controls always survive;
   the decorative legend drops first (<=1024px, alongside the run-neighbor
   buttons below — the legend grew a selection-ring/preview-ring/mixed-line
   swatch on top of the four status swatches, issue #77 follow-up, and no
   longer fits next to a populated run selector below desktop width), then the
   page title (<=1024px too, see below), then the (now redundant) edition label,
   the back-link and the language switch (<=700px). The reading-mode switch is
   two items for this purpose and only one of them is on the ladder: the marker
   for the mode you are already IN sheds (<=768px in English, <=700px in both),
   and the mode you can switch TO sheds at no rung at all. See the <=768px and
   <=700px blocks. */
/* The title used to hold on to 400px, back when the bar carried one control
   fewer. The language switch is ~100px of control, and it cannot be the thing
   that sheds: it is not reachable from anywhere else on the page, and `?lang=`
   is not something to ask a reader to type. The title, by contrast, is named
   twice over already — by the document title in the tab and by the run selector
   right beside it — so it is what yields. Measured on the fullest tablet bar
   (selector, canvas controls, highlight select, language switch): 747px of
   content in Japanese and 858px in English, either of which overflowed the
   701-768px band with the title still in. */
/* One rung above that, added with the language switch: the legend's three
   NON-status samples (selection ring, preview ring, mixed-line rule) shed before
   the legend as a whole. Measured on the fullest header (ms-k1, both neighbour
   buttons, populated selector, canvas controls, legend, language switch): 1324px
   in Japanese, where the legend alone is 479px — the largest item by far, and
   already squeezing the edition label to nothing. The four status swatches stay,
   because they teach the colour/line encoding a reader cannot deduce from the
   page; the rings and the mixed-line sample teach behaviour that selecting or
   hovering anything also teaches. Shedding the three recovers ~240px. */
@media (max-width: 1500px) {
  .legend .note {
    display: none;
  }
}

/* English needs the same ladder one rung earlier, because English labels are
   wider than the Japanese ones it was tuned for: on the same header, "対応ビューワ"
   is 90px and "Correspondence Viewer" 201px, "ハイライト" 157px against
   "Highlight" 184px, and the seven-entry legend 479px against 654px — 1652px of
   content in all. So in English the legend notes hold on only above ~1680px, and
   at desktop widths the three items a reader can do without go as well: the page
   title (named again by the tab and by the run selector), the highlight mode's
   caption (its <select> keeps the same text as title/aria-label) and the "Index"
   back-link (the index is one click away from any run card). That leaves ~1073px
   of content, so nothing is clipped from 1100px up. This is language-conditional
   because the cause is: `lang` is set from the resolved locale before the first
   paint (assets/js/strings.js), so the rule applies exactly when the labels are
   the long ones. */
@media (max-width: 1800px) {
  html[lang="en"] .legend .note {
    display: none;
  }
}

@media (max-width: 1500px) {
  html[lang="en"] header.app-bar h1 {
    display: none;
  }
  html[lang="en"] .highlight-switch .label {
    display: none;
  }
  /* The back-link BY NAME, not "any anchor in the run switch": the reading-mode
     nav is a child of `.run-switch` in viewer.html, and the descendant selector
     that stood here shed the "Parallel" anchor with the "Index" one -- so from
     1500px down an English reader lost the site's only route into
     `compare.html` while a work that HAS no comparison kept its greyed message.
     The prose above always said back-link; only the selector was wider. */
  html[lang="en"] #run-index-link {
    display: none;
  }
}

/* The run-neighbor buttons (issue #57) are the lowest-priority header control:
   they ride on top of the fullest header, so they only appear when there is
   comfortable desktop headroom. Below this breakpoint they shed entirely,
   leaving exactly the selector + canvas header that already fits down to
   320px — readers there switch runs with the selector, which never sheds. */
@media (max-width: 1024px) {
  header.app-bar h1 {
    display: none;
  }
  .run-switch button {
    display: none;
  }
  .highlight-switch .label {
    display: none;
  }
  .legend {
    display: none;
  }
}

/* The one rung the restored mode anchor cost, and it is paid in English only,
   because the deficit is English-only. Measured on the published bars (rv-m2 and
   ms-k4, populated selector + live canvas controls) with the anchor back in: the
   app bar overflows from 701px to 743px in English -- 43px at 701px, falling to
   1px at 743px -- and by 0px at every width from 744px up, at 700px and below,
   and at every width in Japanese, where the same anchor is 24px instead of 44px.
   What yields in that band is the same half the <=700px block sheds for the same
   stated reason: the mode you are IN is restated by the page you are on, so the
   marker and its separator go and the mode you can switch TO stays. That is 56px
   against a 43px deficit. It is deliberately NOT paid by capping the run selector
   (the other slack on this row): the cap ladder below is 230/150/120px and a
   fourth step in the middle of it would have to be bounded on both sides, since
   `html[lang="en"] .run-switch select` outranks the unqualified `<=700px` cap and
   would silently widen the selector below 700px. Round breakpoint rather than 743,
   and the band the tablet note above already names. */
@media (max-width: 768px) {
  html[lang="en"] .mode-switch .current,
  html[lang="en"] .mode-switch .sep {
    display: none;
  }
}

@media (max-width: 700px) {
  header.app-bar {
    gap: 10px;
  }
  header.app-bar .edition {
    display: none;
  }
  /* The "Index" back-link, named by id for the reason given at the 1500px rung:
     as `.run-switch a` this rule also shed the reading-mode anchor nested in the
     same container, in both languages, at every width from here down. */
  #run-index-link {
    display: none;
  }
  /* Sheds with the back-link, and for the same reason: both are navigation the
     reader can reach elsewhere (the run index carries the switch too, at any
     width), and the header must keep the run selector and the canvas controls
     on one row down to 320px. `?lang=` still works when the control is hidden. */
  .lang-switch {
    display: none;
  }
  /* THE MODE SWITCH DOES NOT SHED -- and until this change that sentence stood
     here while the `.run-switch a` rule three declarations up was shedding it,
     so the comment asserted the opposite of what the block did. It does not
     shed because it is the choice the whole control exists to offer AND because
     it is the only route into the parallel reading anywhere on the site:
     `compare.html` occurs in `assets/js/mode.js` and in no other file under
     `site/` (0 occurrences in index.html, index.js, catalog.js, viewer.html).
     That puts it one rung ABOVE the language switch that sheds just above --
     `?lang=` a reader can keep in a URL or a bookmark, whereas a page with no
     link to it is simply gone -- and it is why the mode switch appears on the
     ladder comment at the top of this section only as the item that never
     yields. What DOES shed is the half of the control that is redundant: the
     mode you are IN is already stated by the page you are on (a different
     heading and a different layout), so at this width the bar keeps only the
     mode you can switch TO. Both of that half's states stay, together: the open
     mode's anchor because it is the route, and a closed mode's greyed reason
     because "you cannot read this work in parallel" is the one thing a reader
     cannot infer from the page in front of them -- see the
     `[data-mode]:not(.current)` rule above, which is what makes "together" hold
     against a future rule as well as this one. In English these two are already
     gone from 768px (see the rung above); this rule is what takes them in
     Japanese, where the bar has room until here. Measured: the full control is
     62px and the app bar overflowed 320px by 25px with it; the remaining half
     is 44px in English and 24px in Japanese, and the bar fits at 320px in both
     (the <=400px English cap on the run selector below is what pays for it). */
  .mode-switch .current,
  .mode-switch .sep {
    display: none;
  }
  .run-switch select {
    max-width: 150px;
  }
}

@media (max-width: 520px) {
  .highlight-switch {
    display: none;
  }
}

/* Very narrow phones (down to 320px): tighten the gap/padding and let the
   selector narrow further too. The title is already gone (<=1024px), so what is
   left on the row here is exactly the run selector and the canvas controls. */
@media (max-width: 400px) {
  header.app-bar {
    gap: 8px;
    padding: 0 10px;
  }
  .run-switch select {
    max-width: 130px;
  }
  /* One rung further in English, and the cause is the same one the desktop
     ladder above is language-conditional for: the mode switch's remaining label
     is "Parallel" at 44px against 並列's 24px, which put the 320px bar 6px over.
     The selector is the only item on the row with slack, and 120px still shows
     a run id. Measured at 320/360/375 in both locales. */
  html[lang="en"] .run-switch select {
    max-width: 120px;
  }
}

/* ---- Main split ------------------------------------------------------ */

main.split {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
}

#image-pane {
  flex: 1 1 55%;
  min-width: 220px;
  position: relative;
  background: #20201c;
}

#osd {
  position: absolute !important;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* A canvas the bundle does not carry must not be swapped for another one in
   silence: this bar names the canvas that was asked for and the one being shown
   instead. It sits above the image, not in the text pane, because the image is
   what is wrong. */
.canvas-notice {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  padding: 8px 12px;
  background: #7a2e1e;
  color: #fdf3ee;
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
}

.canvas-notice[hidden] {
  display: none;
}

/* The run seam (issue #184). A reader at Müller volume 2's last canvas had
   `#canvas-next` disabled and no sentence anywhere saying they were at the end
   of a book, and all 54 runs open on their first canvas, so the reader who has
   just crossed a seam is standing on the other one.

   AT THE FOOT OF THE IMAGE, not the top, and deliberately NOT `.canvas-notice`'s
   warm alarm: reaching the end of a run is not a fault, and the two must not read
   as the same kind of statement. Both can be on screen at once (a last leaf that
   carries no box), which is the other reason they are separate elements — see the
   note in viewer.html.

   `pointer-events: none` so the bar never swallows a drag on the image beneath
   it: it holds no control, and the page it covers is one a reader pans. */
.run-seam {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 5;
  padding: 6px 12px;
  background: rgba(29, 27, 22, 0.82);
  color: #e9e6dd;
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  pointer-events: none;
}

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

.image-fallback {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  justify-content: center;
  padding: 24px;
  text-align: center;
  color: #e9e6dd;
  font-size: 13px;
  line-height: 1.7;
}

.image-fallback code {
  background: rgba(255, 255, 255, 0.12);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 12px;
}

.splitter {
  flex: 0 0 6px;
  cursor: col-resize;
  background: var(--line);
}

.splitter:hover {
  background: var(--accent);
}

#text-pane {
  flex: 1 1 45%;
  min-width: 240px;
  overflow-y: auto;
  padding: 12px 12px 18px;
  background: var(--bg);
}

/* ---- The row where the numbering skips -------------------------------- */

/* A passage the SOURCE TEXT does not contain, declared by the run's recipe and
   rendered at its place in reading order. It has to read as NOT A UNIT at a
   glance: no card background, no status rule down the left edge, a dashed border
   and no pointer, because there is nothing here to select. It borrows the
   refuting colour rather than a status colour for the same reason that colour
   exists — "somebody checked and this is wrong" is the most informative thing
   the pane can say — and a status hue would put this hole on the matched /
   partial / ambiguous / unmatched scale, which it is not on. */
.absence-marker {
  border: 1px dashed var(--verify-refuted);
  border-radius: 8px;
  padding: 9px 12px;
  margin-bottom: 9px;
  cursor: default;
  background: color-mix(in srgb, var(--verify-refuted) 6%, transparent);
}

.absence-marker .absence-heading {
  font-size: 13px;
  font-weight: 600;
  color: var(--verify-refuted);
}

.absence-marker .absence-detail {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

/* ---- Text cards ------------------------------------------------------ */

.text-card {
  background: var(--panel-bg);
  border: 1px solid var(--line);
  border-left: 5px solid var(--status-unmatched);
  border-radius: 8px;
  padding: 9px 12px;
  margin-bottom: 9px;
  cursor: pointer;
}

.text-card[data-status="matched"] {
  border-left-color: var(--status-matched);
}
.text-card[data-status="partial"] {
  border-left-color: var(--status-partial);
}
.text-card[data-status="ambiguous"] {
  border-left-color: var(--status-ambiguous);
}
.text-card[data-status="unmatched"] {
  border-left-color: var(--status-unmatched);
  cursor: default;
  opacity: 0.82;
}

/* Hover preview (issue #59): a light accent outline + cool tint. Subordinate to
   .selected, which is declared after this block so its warm background, accent
   border, and ring win when a card is both selected and previewed. Only colour
   and background change, so previewing never shifts a card's dimensions. */
.text-card.preview {
  border-color: var(--accent);
  background: var(--preview-bg);
}

.text-card.selected {
  background: var(--selected-bg);
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* Keyboard focus (issue #63): the focusable button-like control is the .card-body
   (meta + verse) region, not the whole article, so the <details> disclosure stays
   a separate native control rather than a descendant of a role="button" element.
   A distinct accent outline offset just outside that region marks focus.
   :focus-visible scopes it to keyboard focus, so a mouse click that selects a card
   does not also paint a ring. The outline is its own layer, independent of the
   preview/selected backgrounds and box-shadows, so focus stays visible even on a
   selected card and never shifts layout. */
.text-card .card-body:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.text-card .meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 5px;
  font-size: 11.5px;
  color: var(--muted);
}

.text-card .tid {
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.badge {
  font-size: 11px;
  font-weight: 600;
  color: #fff;
  border-radius: 999px;
  padding: 1px 8px;
  background: var(--status-unmatched);
}

.badge[data-status="matched"] {
  background: var(--status-matched);
}
.badge[data-status="partial"] {
  background: var(--status-partial);
}
.badge[data-status="ambiguous"] {
  background: var(--status-ambiguous);
}
.badge[data-status="unmatched"] {
  background: var(--status-unmatched);
}

/* ---- The verification ladder (DESIGN section 7.6) --------------------
   WHAT CHECKED this placement, on every card including the 99.8 % that nothing
   checked. It is NOT a score, so it is never a bar, a meter, a percentage or a
   colour ramp: five discrete states, styled as five discrete things.

   THE ENCODING IS DELIBERATELY NOT A GRADIENT FROM WEAK TO STRONG.
   `unverified` is quiet because "nobody looked" should not shout; the two
   REFUTING rungs are the loudest thing on the card, because somebody checked and
   the box is wrong, and that is the most informative state this site can show. A
   ramp would put `human-refuted` next to `unverified` at the bottom and hide
   exactly the units a reader most needs to distrust. */
.text-card .verify {
  font-size: 11px;
  font-weight: 600;
  border-radius: 4px;
  padding: 1px 6px;
  border: 1px solid currentColor;
  color: var(--verify-unverified);
  background: transparent;
  white-space: nowrap;
  cursor: help;
}

/* The default, and the majority. Outline only, in the muted ink the rest of the
   meta row uses: present and readable, never an alarm. */
.text-card .verify[data-rung="unverified"] {
  color: var(--verify-unverified);
  border-style: dashed;
}

.text-card .verify[data-rung="page-bracket"] {
  color: var(--verify-bracket);
}

.text-card .verify[data-rung="gold"] {
  color: var(--verify-gold);
}

.text-card .verify[data-rung="human-confirmed"] {
  color: #fff;
  background: var(--verify-human);
  border-color: var(--verify-human);
}

/* A check ran and DISAGREED. Filled, not outlined; the one warm alarm colour on
   the card; and marked with a glyph as well as a hue, so it survives a
   colour-blind reader and a monochrome print. `content` is a bare marker rather
   than a word — the word is the label beside it and it is translated. */
.text-card .verify[data-refuted="true"] {
  color: #fff;
  background: var(--verify-refuted);
  border-color: var(--verify-refuted);
}

.text-card .verify[data-refuted="true"]::before {
  content: "\2717\00a0"; /* ✗ + nbsp */
  font-weight: 700;
}

.text-card .verify[data-rung="human-confirmed"]::before {
  content: "\2713\00a0"; /* ✓ + nbsp */
  font-weight: 700;
}

/* A rung this build has no treatment for — a value from a newer bundle, or a
   corrupted one. It keeps the neutral base outline and is NOT given the default's
   dashed border: the label beside it is the raw token, and it must not read as
   `unverified`. */
.text-card .verify[data-unknown="true"] {
  border-style: dotted;
  font-style: italic;
}

/* The run's own ladder in one line, above the cards it counts.
   IT IS NOT IN THE APP BAR, and that was tried first: the bar flex-shrinks its
   items against a measured budget, so the line was ellipsised at every desktop
   width and the clause it dropped was the last one — the count of boxes somebody
   found WRONG, which is the number a reader most needs. Here it has the pane's
   full width and wraps if it must. */
.verify-summary {
  padding: 8px 12px;
  margin-bottom: 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel-bg);
  font-size: 12px;
  color: var(--muted);
  cursor: help;
}

/* The count of boxes something found wrong, in the same alarm colour the refuting
   chips wear, so the two read as one statement. */
.verify-summary .wrong {
  color: var(--verify-refuted);
  font-weight: 700;
}

.text-card .verse {
  font-size: 15px;
  line-height: 1.7;
}

/* A unit this bundle carries with an empty text. Same slot as the verse, not the
   same voice: it is our sentence about the record, so it is smaller, muted and
   italic — and it is there at all because the alternative the card used to have
   was `transliteration`, which is the name of a scheme. */
.text-card .verse-absent {
  font-size: 12px;
  line-height: 1.6;
  font-style: italic;
  color: var(--muted);
}

.text-card .src {
  margin-left: auto;
}

.text-card details {
  margin-top: 6px;
  font-size: 12px;
  color: var(--muted);
}

.text-card details summary {
  cursor: pointer;
}

.text-card details .norm {
  margin-top: 4px;
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 11.5px;
  word-break: break-word;
}

/* ---- External link-out (DESIGN section 9.4) -------------------------- */

/* Per-unit references, for sources whose URL can address a single locus. Like
   the <details> disclosure this is a sibling of the card's button-like region,
   never a descendant of it, so a link is a real link to assistive technology. */
.text-card .external-refs {
  margin-top: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 11.5px;
}

.text-card .external-refs a {
  color: var(--accent);
}

/* Run-scope links sit beside the rights summary, which is the strip that already
   carries edition-level provenance. Fixed-width and never shrinking, so the
   summary ellipsizes first. */
footer.rights-bar .external-links {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The rights dialog lists every declared source, including the ones we do not
   link to; the state chip is what makes "not held there" legible as a different
   claim from "nobody looked". */
.external-state {
  font-weight: 700;
}

.external-state[data-state="linked"] {
  color: var(--status-matched);
}

.external-state[data-state="withheld"],
.external-state[data-state="disabled"] {
  color: var(--muted);
}

.external-note {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--muted);
}

/* ---- Image overlays -------------------------------------------------- */

/* Overlays annotate the page; they must not repaint it. Dense line-level runs
   (ms-k*: thousands of regions per run, most line boxes shared by 2-6 verses) used
   to be drawn as translucent status fills, which failed three ways at once: the
   script underneath was buried, coincident fills stacked their alpha, and the
   stacked green+orange landed on a muddy olive that belongs to no status at all.
   Replacing the fill with a *rule under the line* fixes all three by construction
   — nothing is painted over the script, and nothing can accumulate.

   Reading order of this block (the tier ordering of issues #59/#63 is preserved):
   default (rule only) < preview (accent ring) < selected (white/black rings),
   each state declared after the one it must beat. */

.region-overlay {
  /* The status hue rides on `color`, so every rule below can say `currentColor`
     and keep following the model-driven --status-* variables (applyStatusColors).
     The previous hard-coded rgba() fills silently ignored viewer.status_colors. */
  color: var(--status-unmatched);
  background: transparent;
  border: 0;
  cursor: pointer;
  --rule-h: 3px;
  border-radius: 2px;
  /* Only the one hovered/selected overlay animates. Opacity is deliberately left
     out: the spotlight below toggles it on every overlay at once, and animating
     ~2400 of them on each selection is a stall for no expressive gain. */
  transition:
    background-color 0.12s ease,
    box-shadow 0.12s ease;
}

.region-overlay[data-status="matched"] {
  color: var(--status-matched);
}
.region-overlay[data-status="partial"] {
  color: var(--status-partial);
}
.region-overlay[data-status="ambiguous"] {
  color: var(--status-ambiguous);
}
.region-overlay[data-status="unmatched"] {
  color: var(--status-unmatched);
}

/* The status rule: a bar pinned to the bottom edge of the region, clear of the
   text it belongs to. It is a ::after box, so it costs no layout and stays out of
   the accessibility tree (role="button" keeps no exposed descendant, issue #63).
   The overlay itself is position:absolute (set inline by OpenSeadragon), so this
   positions against the region without the CSS touching OSD's geometry.

   Coincident regions (a shared manuscript line matched to 2-6 verses,
   regionHitKey) used to each draw this rule independently, so n identical
   rectangles stacked n copies of it: same-status groups just wasted paint, but
   mixed-status groups showed only the topmost rule in DOM order, and a dashed
   rule's gaps let a solid rule underneath show through as a pattern absent from
   the legend (issue #77 review). The rule is now drawn once, by the group's
   representative element only — viewer.js and compare.js each set
   data-rule-owner on it — so every other overlay sharing the rectangle stays
   otherwise fully interactive (hover/focus/selection rings) but bare
   underneath. */
.region-overlay::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 0;
  display: none;
}

.region-overlay[data-rule-owner]::after {
  display: block;
  border-bottom: var(--rule-h) solid currentColor;
}

/* Status is encoded twice — hue *and* line style — so the four states stay
   distinguishable without colour vision (and on a photographed, colour-cast
   page). unmatched gets its own line style (double) rather than a plain solid
   rule dimmed by opacity: solid-but-faded read as the same line family as
   matched's solid, and green/grey is one of the pairs colour-vision deficiency
   collapses most readily (DESIGN-CRITIQUE problem 3). Removing the per-status
   opacity also stops it compounding with the spotlight's 45% dim into an
   effective ~31% that all but disappears on parchment (problem 2). The header
   legend swatches carry the same line styles. */
.region-overlay[data-rule-owner][data-status="partial"]::after {
  border-bottom-style: dashed;
}
.region-overlay[data-rule-owner][data-status="ambiguous"]::after {
  border-bottom-style: dotted;
}
.region-overlay[data-rule-owner][data-status="unmatched"]::after {
  border-bottom-style: double;
  --rule-h: 5px; /* `double` needs enough width to render two visible lines */
}

/* Coincident regions intentionally draw on the same full-width line. A previous
   version split one byte-identical line box into horizontal slots so each verse
   could have a separate hit area. That implied the model knew the verse's
   left/right position inside the manuscript line, but it only says "these n
   verses share this line". Click reachability is being handled as behaviour
   elsewhere (issue #78), so this visual layer does not invent position data. */

/* A shared line with more than one distinct status cannot use a single line
   style honestly — there is no one status to encode.

   NO PUBLISHED RUN HAS EVER REACHED THIS RULE, and the sentence that used to
   stand here ("matched+partial is the common case; matched+partial+ambiguous is
   the observed maximum") did not say which corpus it had measured. Re-derived
   over `site/data/` on 2026-09-10 (issue #179): 54 bundles, 193,541 regions,
   every one on a `matched` unit; 0 `partial`, 0 `ambiguous`, and 520 `unmatched`
   units that carry no box by construction. 42,147 of the 137,169 distinct
   rectangles are shared by more than one unit and not one carries a second
   status, so `[data-group-mixed]` is never set on this site. The old sentence
   was true of the matcher's UNPUBLISHED `ms-full` arm — 533 `matched+partial`
   rectangles and exactly one `matched+partial+ambiguous` over three volumes —
   and was written as though it described this corpus.

   The rule is PROVISION, not dead weight: the producing side can and intends to
   publish such a box. `veda-text-image-matching`'s
   `serialize/viewer_bundle.py:659` withholds geometry from `unmatched` alone,
   `:664` REFUSES a `partial` or `ambiguous` unit that has none, and `:721`
   copies the status through beside the box.
   `tests/test_the_published_status_distribution.py` fails the day that reaches
   the site.

   Instead the rule interleaves every distinct status colour in a tight,
   full-width repeating band (leaf.js's buildGroupGradient, which both
   controllers call to set --group-rule-image, referencing var(--status-*) so it
   stays model-driven). The period is small
   (7px per status) and repeats across the *entire* width rather than splitting
   left/right, so — unlike the horizontal-slot design rejected during #77/#78 —
   it never implies that one side of the line belongs to one verse. That the
   line reads as "mixed" at a glance is intentional: a shared line carrying a
   verse the matcher was only `partial` or `ambiguous` about is exactly what a
   researcher needs to notice. (Not "an unmatched verse", which is what this
   said and cannot happen in any corpus: an `unmatched` unit has no geometry, so
   it is on no line at all — `ingest.py:414` and `viewer_bundle.py:659` both
   refuse it.) */
.region-overlay[data-rule-owner][data-group-mixed]::after {
  /* The single-status rule above gets its rendered height from the classic
     "height:0 + border-bottom" trick: box-sizing:border-box cannot shrink an
     already-rendered border below its own width, so the box paints only the
     border's height, positioned flush with the region's own bottom edge. That
     trick cannot carry a multi-colour background, though: with the content
     height collapsed to 0, the background positioning area (background-origin
     defaults to padding-box, sized from content+padding) also collapses to
     zero, so a background-image painted "under" a transparent border has
     nothing to paint into and never appears (confirmed empirically — this is
     not a theoretical concern). A real `height` avoids that collapse and
     still lands in the same place: with `bottom: 0` (inherited from the base
     rule) and an explicit height, absolute positioning puts the box's bottom
     edge at the region's own bottom edge and its top edge `height` above that
     — i.e. inside the region's last --rule-h pixels, exactly where the
     single-status rule paints, not below/outside it. That match matters: the
     measured line pitch in the dense runs is near zero (line boxes are
     adjacent or slightly overlapping), so anything drawn *below* a region
     would land on the next line's text. */
  border-bottom: none;
  height: var(--rule-h);
  background-image: var(--group-rule-image);
}

body[data-highlight="selected"] .region-overlay:not(.selected):not(.preview) {
  color: transparent;
  background: transparent;
  border-color: transparent;
}

/* Sparse canvases (measured as no overlapping regions) get a FRAME. They used to
   get a 12% fill as well, and issue #85 is why they no longer do.

   THE FILL COULD NOT CARRY THE HUE IT WAS DERIVED FROM. `color-mix(in srgb,
   currentColor 12%, transparent)` is currentColor at alpha 0.12, so the pixel a
   reader actually sees is 88% page. Re-derived 2026-09-10 by
   `scripts/census-of-the-status-colour-separation.py` — Machado/Oliveira/
   Fernandes (2009) severity 1.0, ΔE CIE76, composited in gamma-encoded sRGB over
   the mean rule band of #85's page sample (Y = 0.665) — as the minimum pairwise
   ΔE over the three statuses a leaf can actually draw:

     the 3px rule, at rest ........ 13.4   (protanopia, matched/partial)
     under the 45% spotlight ...... 12.3   (protanopia, matched/partial)
     inside this 12% fill .......... 3.1   (protanopia, matched/partial)

   The fill discarded 77% of the resting separation and landed at 1.35× the CIE76
   JND of 2.3. Its own contrast against the same band was 1.12–1.16 : 1, and
   1.06–1.11 : 1 on the darkest page in that sample (`ts-v1`, Y = 0.353) — below
   any threshold at which it is a mark at all. What it did do reliably was paint
   over the script, which is the one thing the head of this block says an overlay
   must not do.

   AND IT WAS NOT LOAD-BEARING FOR FINDABILITY, which is what it was there for.
   Over the median published box (1204 × 91) the fill painted 100% of the area,
   all of it over script; the 2px frame paints 4.7% and none of the interior. No
   sparse box loses its status mark either: a rectangle shared by two units is two
   coincident rectangles, which overlap, so a sparse canvas cannot have one —
   measured, 0 shared rectangles among the 22,768 regions on the 5,415 sparse
   canvases — and every one of those regions is therefore its own
   `data-rule-owner` and draws the 3px rule.

   REMOVING IT IS ALSO THE ANSWER TO #83, which is why that issue changed no
   threshold. With the fill gone, sparse and dense canvases carry the SAME status
   grammar — hue plus line style, on the rule — and differ only by an added
   outline, so the boundary flipping under a 1px sliver no longer switches the
   language a reader has to read. See `isSparseCanvas` in assets/js/leaf.js.

   JS sets data-sparse per canvas after measuring. Declared before the state rules
   below so preview/selected still win. The frame is a real border (box-sizing is
   border-box, so it grows inward and never moves an OSD-positioned region) rather
   than a ring, so it can carry the same solid/dashed/dotted status encoding the
   rule does — the double encoding must not disappear just because a canvas is
   sparse. */
.region-overlay[data-sparse] {
  border: 2px solid currentColor;
}

.region-overlay[data-sparse][data-status="partial"] {
  border-style: dashed;
}
.region-overlay[data-sparse][data-status="ambiguous"] {
  border-style: dotted;
}

/* AND THERE IS DELIBERATELY NO `[data-sparse][data-status="unmatched"]` RULE.
   Issue #85 reads its absence as a hole in the double encoding — matched and
   unmatched would both draw a solid frame — and it is the pair behind #85's two
   worst composited numbers (the fill's ΔE 3.1 under deuteranopia and 1.6 under
   tritanopia are both `unmatched` pairs, as is the dim's 6.2).

   THE PAIR CANNOT BE DRAWN, in any corpus, and not merely "has not been yet".
   An `unmatched` unit has no geometry by construction at BOTH ends of the
   pipeline: `ingest.py` raises if an unmatched unit carries a region and raises
   if any other status carries none (pinned by
   `tests/test_the_published_status_distribution.py`'s
   `test_ingest_refuses_geometry_on_an_unmatched_unit` and
   `test_ingest_requires_geometry_for_every_status_but_unmatched`), and the
   producing side holds the same contract by its own means —
   `serialize/viewer_bundle.py:659` DROPS the geometry of an unmatched unit and
   counts the contradiction rather than raising on it, while `:664` refuses a
   `partial` or `ambiguous` unit that has none — so an unmatched box neither
   reaches a bundle nor survives being read from one. No
   `.region-overlay[data-status="unmatched"]` element is ever created. That is a stronger statement than #179's about `partial` and
   `ambiguous`, which are unpublished-so-far and whose rules above are therefore
   real provision: a frame rule for `unmatched` would be provision for nothing.
   The three statuses this frame can meet are matched (solid), partial (dashed)
   and ambiguous (dotted), and all three are here.

   The `double` rule on the `::after` further up is in exactly the same position
   and is deliberately left where it is: deleting unreachable rules is #179's
   territory and #179 is closed. It is named here so that a later reader does not
   read the asymmetry as an oversight and add this rule's missing twin. Note that
   it is NOT what draws the legend's unmatched specimen — `.legend i` has its own
   `double`, on an element the model always emits a swatch for, because the text
   pane does show unmatched units even though no leaf can. */

/* ---- A box a person read and REFUTED -----------------------------------

   NOT ON THE STATUS SCALE, and drawn so. matched / partial / ambiguous /
   unmatched are the matcher's four words about one box; "somebody opened this
   page and this box is wrong" is a different axis, and giving it a fifth status
   hue would rank it against the four instead of overriding them. So it takes the
   pane's existing refutation vocabulary — the dashed `--verify-refuted` frame
   `.absence-marker` and `.column-state[data-state="declared-absent"]` already
   use — and the reader learns one treatment for one meaning across both panes.

   Until issue #82 the ladder reached the text card and stopped. A box drawn on
   Schroeder's German critical apparatus rather than on Sanskrit verse carried a
   `matched` rule in solid green, identical to a box nobody had ever looked at,
   while the refutation sat in the bundle unable to reach the page.

   AND THE SIZE OF THAT IS TWO NUMBERS, NOT ONE, BECAUSE A BOX IS A RECTANGLE.
   One `.region-overlay` is one `region` of one unit, and the matcher maps
   several units onto the same printed line — so the Maitrāyaṇī's refutations are
   **83 rectangles carrying 148 refuted units** (`ms-k1` 42 rectangles / 66 units,
   `ms-k4` 41 / 82). This comment read "148 of them are published today", which is
   the UNIT count wearing the word "box", and it cited `conf/ms-k1.toml` — a file
   documenting `ms-k1`'s own 91 rows and 66 refutations and never the pair.
   `conf/ms-k3.toml` [verification] says it correctly: "83 of their lines — 148
   units". The distinction is not pedantry: somebody auditing whether this frame
   renders will open the Maitrāyaṇī expecting 148 red dashed frames, count 83, and
   file a regression that does not exist — inside the one block whose whole
   purpose is making a silent cascade failure visible.

   Site-wide the same pair is 133 rectangles carrying 201 refuted units. Every
   figure in this block is re-derived by `scripts/census-of-the-published-boxes.py`
   (2026-09-10); run it rather than believing it.

   The frame is on the OVERLAY, not the rule, because it is a claim about this
   rectangle and it must survive the rule-owner gate: a refuted unit sharing a
   line with five others is still refuted even when a sibling owns the rule.
   box-sizing is border-box, so the frame grows inward and moves no geometry. */
.region-overlay[data-verify="human-refuted"],
.region-overlay[data-verify="page-bracket-refuted"] {
  border: 2px dashed var(--verify-refuted);
  border-radius: 3px;
}

/* The shared rule turns refuting only for `n of n`. A line carrying one
   refutation among six units has not been refuted as a line, and a rule is one
   statement about the whole line — the same argument that stops a mixed-status
   group from borrowing one status's line style. viewer.js and compare.js set
   `data-group-refuted` for n of n and `data-group-part-refuted` otherwise, and
   `data-group-refuted-count` carries k for anything that wants to say it. */
.region-overlay[data-rule-owner][data-group-refuted] {
  color: var(--verify-refuted);
}

/* THE RULE KEEPS ITS STATUS'S LINE STYLE and changes only hue. Taking a fifth
   line style would put refutation on the four-status scale, which is what this
   block exists not to do — and the four styles are already spoken for. The
   double encoding this file requires is still there, one axis per claim: the
   line STYLE says what the matcher found, and the dashed FRAME plus the refuting
   hue say that a person read the page and disagreed. Without colour vision the
   frame alone carries it, which is why the frame is not decorative.

   (Whether the four statuses' own double encoding survives a colour-vision
   simulator is issue #85 and is not claimed here.) */

/* A refuted box inside a mixed group must not have the group's status gradient
   painted over its own frame; the gradient owns the rule, the frame owns the
   rectangle, and they do not compete. Declared for the record rather than to
   change anything: the gradient is a background-image on the ::after, so it
   cannot reach the border box. */
.region-overlay[data-group-mixed][data-verify="human-refuted"] {
  border-color: var(--verify-refuted);
}

/* A REFUTED BOX KEEPS ITS REFUTING FRAME ON A SPARSE CANVAS TOO, and this rule
   exists because the block below could not do it alone. `[data-sparse]`'s
   `border` SHORTHAND above is 0-2-0 and so is `[data-verify="human-refuted"]`,
   so whichever is written later wins: with the refuted block declared before the
   sparse frame, the red dashed frame was replaced by a 2px solid status-coloured
   one on every sparse canvas, silently, and **15 of the site's 133 published
   refuted rectangles are on sparse canvases** (hirgs-v1 11, rv-m1 2,
   sankhgs-v1 1, ts-v2 1). The denominator read 189 and 189 is not a population of
   boxes at all: it is the number of verification ROWS the Maitrāyaṇī catch-up
   filed (91 + 7 + 91), 41 of them CONFIRMATIONS, and none of those 41 is on a
   sparse canvas. Rectangles is the population this sentence is about, because a
   rectangle is what this rule draws a frame on; the same 15 are also 15 refuted
   units, since a sparse canvas is one where no two rectangles overlap. Ordering
   the block after the sparse rules fixes the base case; this 0-3-0 pair is what
   beats the per-status `border-style` overrides just above, so an ambiguous or
   partial sparse box cannot take back the frame's line style either.

   Pinned by tests/browser/viewer-smoke.test.js, because the failure was a
   cascade order that no unit test can see. */
.region-overlay[data-sparse][data-verify="human-refuted"],
.region-overlay[data-sparse][data-verify="page-bracket-refuted"] {
  border: 2px dashed var(--verify-refuted);
}

/* ---- A rung this build CANNOT READ -------------------------------------

   `data-verify` carries the bundle's token verbatim (model.js: an unrecognised
   rung is rendered as itself, never mapped onto `unverified`), so a typo or a
   newer vocabulary -- `human-refutde` -- matches neither refuting selector
   above. Without this rule such a box paints like the base overlay, which is
   exactly how a box NOBODY HAS LOOKED AT paints: unreadable evidence laundered
   into the most reassuring appearance, which is the failure the whole refuted
   treatment exists to end. Both controllers set `data-unknown="true"` beside
   the raw token, the same attribute the text card's chip carries for the same
   reason.

   WHAT IT LOOKS LIKE, and why each of the three things it must not look like is
   excluded:

   - NOT UNMARKED. An unmarked box carries no frame at all (`border: 0` on the
     base rule), and a checked-and-fine box carries none either -- the ladder
     draws nothing for `gold`, `page-bracket` or `human-confirmed`, because
     agreeing with the aligner is not news. So any frame at all is the statement
     "there is a verification row on this box", and that is the one thing this
     mark may assert.
   - NOT REFUTED. Different on BOTH channels this file requires, not just hue:
     dotted rather than dashed, and the neutral ink rather than the one warm
     alarm colour on the site. A reader without colour vision still separates the
     two, which a hue-only difference would not give them.
   - NOT VERIFIED, and not a status either. The ink is `--verify-unverified`, the
     one neutral on the ladder, because this mark makes NO claim about the
     placement -- only about this build's ability to read the evidence. Any
     louder hue would rank it against the four statuses or against the
     refutation, and it belongs to neither scale. It is the same ink and the same
     dotted line the card gives its unknown chip, so the two panes teach one
     vocabulary for one meaning; the token itself is named in the box's `title`,
     as it is in the chip's label.

   TWO ATTRIBUTES, SO 0-3-0, and placed here for the trap the block above
   records: `[data-sparse]`'s `border` shorthand is 0-2-0 and the per-status
   `[data-sparse][data-status=...]` overrides are 0-3-0, so this needs both the
   higher specificity AND a position after them -- a 0-2-0 `[data-unknown]`
   alone, or this rule written before the sparse ones, loses the frame on every
   sparse canvas exactly as the refuted frame did. It needs no sparse-qualified
   twin for the same reason: it already beats the shorthand outright. */
.region-overlay[data-verify][data-unknown="true"] {
  border: 2px dotted var(--verify-unverified);
  border-radius: 3px;
}

/* Hover/focus preview (issue #59): stronger than the resting rule, deliberately
   weaker than selection. It keeps the cool accent identity of the card preview,
   which also stops it from ever being mistaken for a status colour.
   The declared tier order is default < preview < selected, but on parchment a
   saturated cool blue reads as *more* salient than an achromatic white/black
   ring — chroma beats a pure lightness contrast for the eye — so the ring was
   perceptually outranking selection (DESIGN-CRITIQUE problem 3 / "もし1つだけ
   直すなら" runner-up). Thinning it to 1px and softening it with color-mix keeps
   the accent identity legible while it stops competing with .selected below. */
.region-overlay.preview {
  z-index: 4;
  --rule-h: 5px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 80%, transparent),
    0 0 0 1px rgba(255, 255, 255, 0.75);
}

/* Selection is hue-free. The status rule may still thicken, but the selected
   state itself is the white/black contrast sandwich from proposal B: white inset
   keyline, white outer ring, then a near-black outer ring. It reads on parchment,
   ink, and any status colour beneath it, while leaving the manuscript text inside
   the region unpainted. */
.region-overlay.selected {
  z-index: 5;
  --rule-h: 6px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.95),
    0 0 0 2px rgba(255, 255, 255, 0.95),
    0 0 0 4px rgba(18, 18, 18, 0.92);
}

.region-overlay.selected::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0;
  box-shadow: 0 0 0 3px rgba(18, 18, 18, 0.6);
  animation: region-select-pulse 0.75s ease-out 1;
}

@keyframes region-select-pulse {
  from {
    opacity: 0.85;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(1.07);
  }
}

@media (prefers-reduced-motion: reduce) {
  .region-overlay {
    transition: none;
  }
  .region-overlay.selected::before {
    animation: none;
  }
}

/* Spotlight: while a selected region is actually on screen, the rest fade back so
   the selection is unmistakable in a crowd. They fade, never disappear — the
   surrounding correspondences stay readable as context, which is the whole point
   of the dense view. body.has-selection is set from what is painted, so selecting
   an unmatched verse (no region) never fades the page for nothing.

   ISSUE #85 ASKED WHETHER THIS STATE CAN CARRY THE STATUS DISTINCTION, since it
   is the state a reader is in for as long as a verse is selected. IT CAN, and
   this is the recorded answer rather than a change. Re-derived 2026-09-10 by
   `scripts/census-of-the-status-colour-separation.py`, minimum pairwise ΔE76
   under Machado severity 1.0 over the three statuses a leaf can draw (matched /
   partial / ambiguous — `unmatched` has no geometry at either end of the
   pipeline, see the sparse block above):

     at rest ................ 13.4      (protanopia, matched/partial)
     dimmed to 45% .......... 12.3      (protanopia, matched/partial)

   The dim costs 1.1 ΔE units, which is itself below the CIE76 JND of 2.3. #85's
   headline 6.2 is the ambiguous/unmatched pair under tritanopia and is not a
   state this viewer can render. And `opacity` does not touch geometry, so all
   four line styles survive the dim exactly as drawn — the second channel is
   untouched. The one state where the dim genuinely was lossy was compounded with
   the old 12% sparse fill, because a fill has no line style and hue was its only
   channel; that composite no longer exists.

   WHAT IS NOT CLAIMED HERE IS CONTRAST. Dimmed, every status sits at
   1.57–1.80 : 1 over the mean rule band and 1.26–1.47 : 1 over the darkest page
   in #85's sample. That is the spotlight working as designed — pushing the
   unselected back is the point — but that band luminance is measured elsewhere:
   #85 fetched ten of this site's canvases over IIIF and sampled them, and no
   committed code re-derives it. It bounds nothing site-wide and nothing here
   should be read as saying it does. */
body.has-selection .region-overlay:not(.selected):not(.preview) {
  opacity: 0.45;
}

/* Keyboard focus (issue #63): an accent outline offset outside the region.
   Outlines paint after box-shadows, so it draws on top of the selected state's
   white halo rather than being lost in it, and it stays visible on a previewed or
   selected overlay. :focus-visible keeps it to keyboard focus; outline-offset
   means the focus ring never resizes or moves the region. A focused overlay is
   also previewed (focus feeds the preview machinery), so it is never dimmed by
   the spotlight above. */
.region-overlay:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  z-index: 6;
}

/* ---- Rights footer + panel ------------------------------------------ */

footer.rights-bar {
  height: var(--footer-h);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 14px;
  background: var(--panel-bg);
  border-top: 1px solid var(--line);
  font-size: 12px;
  color: var(--muted);
}

footer.rights-bar .nc {
  /* The non-commercial badge is a legal requirement, not a decoration: it must
     never shrink or wrap, whatever else the bar has to fit. */
  flex: 0 0 auto;
  white-space: nowrap;
  font-weight: 700;
  color: var(--status-partial);
  border: 1px solid var(--status-partial);
  border-radius: 4px;
  padding: 1px 6px;
}

footer.rights-bar .grow {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
footer.rights-bar a {
  color: var(--accent);
}

footer.rights-bar button {
  font: inherit;
  border: 1px solid var(--line);
  background: var(--bg);
  border-radius: 6px;
  padding: 3px 10px;
  cursor: pointer;
  white-space: nowrap;
}

dialog.rights-detail {
  border: 1px solid var(--line);
  border-radius: 10px;
  max-width: 640px;
  width: calc(100% - 40px);
  padding: 0;
  color: var(--ink);
}

dialog.rights-detail::backdrop {
  background: rgba(0, 0, 0, 0.45);
}

dialog.rights-detail .head {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
}

dialog.rights-detail .head h2 {
  font-size: 15px;
  margin: 0;
  flex: 1 1 auto;
}

dialog.rights-detail .body {
  padding: 14px 16px;
  font-size: 13px;
  line-height: 1.65;
}

dialog.rights-detail h3 {
  font-size: 13px;
  margin: 14px 0 4px;
}

dialog.rights-detail dl {
  margin: 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 2px 12px;
}

dialog.rights-detail dt {
  color: var(--muted);
}
dialog.rights-detail dd {
  margin: 0;
  word-break: break-word;
}
dialog.rights-detail a {
  color: var(--accent);
}

dialog.rights-detail .close {
  font: inherit;
  border: 1px solid var(--line);
  background: var(--bg);
  border-radius: 6px;
  padding: 3px 12px;
  cursor: pointer;
}

.status-msg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  text-align: center;
  font-size: 14px;
  color: var(--muted);
}

/* ---- index.html run picker ------------------------------------------ */

body.index {
  padding: 32px 20px;
  max-width: 760px;
  margin: 0 auto;
  line-height: 1.6;
}

/* The page title and the language switch share one row; the switch wraps under
   the title on a narrow phone rather than shedding (this page, unlike the
   viewer's app-bar, has the room and is where a phone reader changes language). */
body.index .page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 16px;
}

body.index h1 {
  font-size: 20px;
}
body.index .run-list {
  list-style: none;
  padding: 0;
}

body.index .run-list a {
  display: block;
  padding: 12px 14px;
  background: var(--panel-bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-bottom: 10px;
  text-decoration: none;
  color: var(--ink);
}

body.index .run-list a:hover {
  border-color: var(--accent);
}

/* Work groups (assets/js/index.js): the cards sit under one heading per work,
   because twenty-five runs over six works — fourteen of them Śatapatha kāṇḍas —
   made a reader looking for the one-volume Kāṭhaka scroll past all fourteen (as the
   site stood on 2026-08-20, when the grouping landed: those are the figures that
   motivated it, deliberately not restated against today's). The
   heading is the quietest thing that can still separate groups: a rule under a
   line of text, below the page title (20px) in size, and no box, so the cards
   stay the only cards on the page. */
body.index .work-group + .work-group {
  margin-top: 24px;
}

body.index .work-heading {
  font-size: 15px;
  font-weight: 600;
  margin: 0 0 10px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
}

/* Edition groups (issue #183): inside a work, the cards sit under one heading per
   `edition_id`. Quieter than the work heading it nests under — no rule, no bold
   weight on the frame — because the work heading is what a reader scans for and
   this is the level below it. The list needs its own class rather than a second
   `.run-list`: a nested list of the same name would make the aria-labelledby
   assertion on the work's list ambiguous, and the two lists name different
   headings. */
body.index .edition-group + .edition-group {
  margin-top: 14px;
}

/* Normal inline flow, NOT flex. The spacing between the caption, the id and the
   count is real text in the DOM (assets/js/index.js says why): a flex `gap` is
   invisible to a screen reader and to a copy-paste, and this heading is three
   values that must not run together into one word. */
body.index .edition-heading {
  font-size: 12px;
  font-weight: 400;
  margin: 0 0 6px;
  color: var(--muted);
}

/* The caption on the id, and the id itself. The caption is chrome and is
   translated; the slug is a catalog value and is quoted as it stands, which is
   why it is the one thing on the line carrying any weight (issue #185 drew the
   same line on compare.html). No `text-transform` on either: a presentational
   rule rewriting a data value is the same mistake as translating one. */
body.index .edition-heading .edition-caption {
  font-size: 10px;
  letter-spacing: 0.02em;
}

body.index .edition-heading .edition-id {
  font-weight: 700;
  color: var(--ink);
}

/* How many RUNS this site publishes the edition as — never a volume count, since
   no field anywhere carries one (issue #182). Pushed to the end of the line so it
   reads as a tally of the cards below it rather than as part of the id. */
body.index .edition-heading .edition-run-count {
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

body.index .edition-runs {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The one sentence that says the run counts on this page are not volume counts.
   Sits between the list heading and the first card, at the width of the page, so
   it is read before the numbers it qualifies rather than after them. */
body.index .list-note {
  margin: -4px 0 14px;
}
body.index .note {
  font-size: 13px;
  color: var(--muted);
}

/* ---- index.html: the Veda x stratum grid ----------------------------- */
/* The page's primary discovery surface (assets/js/index.js). A real table drawn
   on `VEDA_ORDER` x `STRATUM_ORDER` (assets/js/catalog.js), most of whose cells
   are empty — and the SPARSENESS IS THE CONTENT, so
   nothing here hides an empty cell or shrinks it out of the way. The page is
   760px wide at most, which with `table-layout: fixed` leaves ~132px a column:
   enough for `Śatapatha-Brāhmaṇa (Mādhyandina)` to wrap onto two or three lines
   and never enough to push the page sideways, because `overflow-wrap: anywhere`
   makes the fixed columns unable to overflow rather than merely unlikely to. */
body.index .veda-grid-wrap {
  margin: 20px 0 28px;
}

/* The page's two section headings (h2): the grid and the run list. A rung above
   the per-work headings (h3, 15px, ruled) that sit inside the list, so the outline
   a reader sees matches the one the markup declares. */
body.index .grid-heading,
body.index .list-heading {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 10px;
}

body.index .list-heading {
  margin-top: 28px;
}

body.index table.veda-grid {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  font-size: 12px;
}

body.index .veda-grid caption {
  caption-side: top;
  text-align: left;
  font-size: 12px;
  color: var(--muted);
  line-height: 1.5;
  padding-bottom: 8px;
}
body.index .veda-grid caption span {
  display: block;
}

body.index .veda-grid th,
body.index .veda-grid td {
  border: 1px solid var(--line);
  padding: 6px 8px;
  text-align: left;
  vertical-align: top;
  overflow-wrap: anywhere;
}

/* Both axes are quieter than the works they index: the reader is looking for an
   edition, and the labels are there to say where it sits. `overflow-wrap: normal`
   undoes the `anywhere` above for these cells ONLY: a work title may be broken
   mid-word to keep a fixed column from overflowing, but `Brāhmaṇa` split as
   `Brāhmaṇ / a` is a mangled Sanskrit word in a header, and these labels are
   short enough to be given the room instead. */
body.index .veda-grid thead th,
body.index .veda-grid tbody th[scope="row"],
body.index .veda-grid tfoot th[scope="row"] {
  font-size: 11px;
  font-weight: 600;
  background: var(--panel-bg);
  color: var(--muted);
  overflow-wrap: normal;
}

/* With `table-layout: fixed` the first cell of the first row sets the row-label
   column and the five Veda columns split what is left equally. In px, not em:
   the corner is a <td> at 12px and the row labels are <th> at 11px, so one `em`
   value would give the two different widths and the wider would silently win. */
body.index .veda-grid .corner {
  width: 78px;
  border-color: transparent;
}
body.index .veda-grid tbody th[scope="row"],
body.index .veda-grid tfoot th[scope="row"] {
  width: 78px;
}

body.index .veda-grid .cell-works {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
body.index .veda-grid .cell-works a {
  font-weight: 600;
  text-decoration: none;
}
body.index .veda-grid .cell-works a:hover,
body.index .veda-grid .cell-works a:focus-visible {
  text-decoration: underline;
}
/* The two numbers under a work in a cell (issue #183): how many EDITIONS of it
   this site publishes, which is what the caption promises, and how many runs
   those are published as, which is what the link lands on. One line, the block
   that `.runs` used to be, so the cell's height is unchanged where a work has one
   edition and one run. */
body.index .veda-grid .cell-works .cell-counts {
  display: block;
  font-size: 10px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
body.index .veda-grid .cell-works .runs,
body.index .veda-grid .cell-works .editions {
  font-size: inherit;
  color: inherit;
  font-variant-numeric: inherit;
}

/* An empty cell. Quiet, but never absent and never a dash or a zero: the text
   says "not published here", which is a statement about this site and not about
   the literature (assets/js/strings.js `grid.notPublished`). */
body.index .veda-grid td.empty {
  color: var(--muted);
  font-size: 10.5px;
  line-height: 1.4;
}

/* Only rendered when some work declares no Veda or stratum — never in the
   published catalog today, which is the point of it. */
body.index .veda-grid td.unclassified .unclassified-note {
  margin: 0 0 6px;
  color: var(--muted);
}

/* Phones and small tablets: the table REFLOWS into one block per stratum rather
   than scrolling sideways. Six columns cannot be read at 320px — the row label
   plus five Vedas would leave ~40px a column, and a work title would break into
   a stack of syllables — so each row becomes a card, the column headers move
   off-screen (they are still announced by `scope`, and each cell repeats its own
   Veda from `data-veda`), and every cell is still there, empty ones included. Losing them would lose the only thing the grid says that
   the list below does not. */
@media (max-width: 700px) {
  body.index .veda-grid,
  body.index .veda-grid thead,
  body.index .veda-grid tbody,
  body.index .veda-grid tfoot,
  body.index .veda-grid tr,
  body.index .veda-grid th,
  body.index .veda-grid td {
    display: block;
  }

  /* Off-screen, not `display: none`: the column headers are what `scope="col"`
     resolves to, so removing them from the box tree must not remove them from
     the accessibility tree. */
  body.index .veda-grid thead {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  body.index .veda-grid tbody tr,
  body.index .veda-grid tfoot tr {
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 8px 10px;
    margin-bottom: 10px;
  }

  /* A <caption> keeps `display: table-caption` unless it is reset too, and in a
     table that is no longer a table it then shrink-wraps to a column of single
     words. */
  body.index .veda-grid caption {
    display: block;
  }

  body.index .veda-grid th,
  body.index .veda-grid td {
    border: 0;
    padding: 0;
    width: auto;
  }

  /* `width: auto` again, at the specificity of the fixed-layout rule above:
     the attribute selector there would otherwise keep the 78px column width and
     break the stratum name across two lines in a block that has the whole
     viewport to spend. */
  body.index .veda-grid tbody th[scope="row"],
  body.index .veda-grid tfoot th[scope="row"] {
    width: auto;
    background: none;
    font-size: 13px;
    color: var(--ink);
    padding-bottom: 6px;
  }

  /* Veda label and works on one line, wrapping to two when the pair is too wide
     for the viewport rather than clipping or scrolling. */
  body.index .veda-grid tbody td {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 8px;
    padding: 3px 0;
  }
  /* 9em is the widest label, `Yajurveda (White)`, so all five line up in a
     column instead of each taking its own width and leaving the cells ragged.
     `flex: 0 0 auto` still lets it exceed that rather than truncate. */
  body.index .veda-grid tbody td[data-veda]::before {
    content: attr(data-veda);
    flex: 0 0 auto;
    min-width: 9em;
    font-size: 10.5px;
    color: var(--muted);
  }
  body.index .veda-grid .cell-works {
    flex: 1 1 9em;
    gap: 4px;
  }
  /* The counts follow the work's name on the same line here rather than sitting
     under it: the stacked layout already spends a line on the Veda label. */
  body.index .veda-grid .cell-works .cell-counts {
    display: inline;
    margin-left: 6px;
  }
}

/* No-JS fallback note under the static run card, one line per language (the
   language switch is script-rendered, so a reader without JS cannot pick one).
   assets/js/index.js clears #run-list, so this disappears the moment it runs. */
body.index .run-list .nojs-note {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 12px;
  color: var(--muted);
  padding: 0 2px;
}
body.index a {
  color: var(--accent);
}

/* Non-fatal catalog-load notice (data/index.json missing or malformed). */
body.index .catalog-status {
  background: var(--selected-bg);
  border: 1px solid var(--status-partial);
  border-radius: 6px;
  padding: 8px 12px;
  color: var(--ink);
}

/* Run cards rendered from the catalog (assets/js/index.js). */
body.index .run-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
}
body.index .run-head .rid {
  font-weight: 700;
}
body.index .run-head .range {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
body.index .run-card .desc {
  font-size: 13px;
  color: var(--muted);
  margin-top: 2px;
}

body.index .run-stats {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--muted);
}
body.index .run-stats .counts {
  font-variant-numeric: tabular-nums;
}

body.index .status-chips {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
}
body.index .status-chips i {
  font-style: normal;
  font-size: 11px;
  line-height: 1.6;
  white-space: nowrap;
  border: 1px solid var(--status-unmatched);
  color: var(--status-unmatched);
  border-radius: 999px;
  padding: 0 8px;
}
body.index .status-chips i[data-status="matched"] {
  border-color: var(--status-matched);
  color: var(--status-matched);
}
body.index .status-chips i[data-status="partial"] {
  border-color: var(--status-partial);
  color: var(--status-partial);
}
body.index .status-chips i[data-status="ambiguous"] {
  border-color: var(--status-ambiguous);
  color: var(--status-ambiguous);
}
body.index .status-chips i[data-status="unmatched"] {
  border-color: var(--status-unmatched);
  color: var(--status-unmatched);
}

body.index .rights-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
body.index .rights-badges .badge {
  font-size: 11px;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 1px 7px;
}
body.index .rights-badges .nc {
  font-size: 11px;
  font-weight: 700;
  color: var(--status-partial);
  border: 1px solid var(--status-partial);
  border-radius: 4px;
  padding: 1px 7px;
}

/* ---- Alternative placements ------------------------------------------
   Several boxes for one text unit, each named by the placement that produced
   it. The visual language is deliberately DIFFERENT from the status hues and
   from the verification ladder's, and deliberately NOT a scale:

   - the run's own box keeps its status hue and its solid treatment, because it
     is the one the bundle publishes and the one every other surface refers to;
   - an alternative is drawn as a dashed outline in one neutral ink -- one ink
     for all arms, so no arm can be read as warmer, stronger or better than
     another. Which arm a box belongs to is said in WORDS, on the card and in the
     overlay's title, never in a hue;
   - the entry the reader is currently looking at is marked by weight, not hue.

   `--placement-ink` is intentionally not a --status-* and not a --verify-*: it
   must not be mistaken for a state of the match or for a rung of the ladder. */
:root {
  --placement-ink: #5a5347;
  --placement-active: #1d1b16;
}

.text-card .placements {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px dashed var(--line);
  font-size: 12px;
}

.text-card .placements-head {
  color: var(--muted);
  font-weight: 600;
  cursor: help;
}

.text-card .placement-list {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.text-card .placement {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
}

.text-card .placement-pick {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font: inherit;
  color: var(--placement-ink);
  background: transparent;
  border: 1px dashed var(--placement-ink);
  border-radius: 4px;
  padding: 1px 6px;
  cursor: pointer;
  text-align: left;
}

.text-card .placement-pick:hover,
.text-card .placement-pick:focus-visible {
  background: rgba(0, 0, 0, 0.04);
}

/* The box currently being looked at. Weight and a solid edge, no hue: this says
   "you are here", which is not a claim about the box. */
.text-card .placement[data-active] .placement-pick {
  color: var(--placement-active);
  border-style: solid;
  font-weight: 600;
}

.text-card .placement-page {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* "This run publishes it" -- a statement about the recipe, not about the box.
   Plain muted text on purpose: a badge would compete with the ladder chip beside
   it and would be read as a verdict. It lives INSIDE the pick button so it can
   never wrap onto a line of its own and caption the entry below. */
.text-card .placement-chosen {
  color: var(--muted);
  font-size: 11px;
  white-space: nowrap;
}

/* Keep an entry on one line where it fits, and let the arm label be the part
   that wraps: the page number and the "published" mark are short and are what a
   reader scans for. */
.text-card .placement-pick {
  max-width: 100%;
  flex-wrap: wrap;
}

.text-card .placement-empty .placement-arm {
  color: var(--placement-ink);
}

.text-card .placement-nobox {
  color: var(--muted);
  font-size: 11px;
  font-style: italic;
}

.text-card .placements-note {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: 11px;
  line-height: 1.5;
  cursor: help;
}

/* The run-level line, beside the ladder's and styled like it so the two read as
   two facts about the run rather than one compound claim. */
.placements-summary {
  padding: 8px 12px;
  margin-bottom: 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel-bg);
  font-size: 12px;
  color: var(--muted);
  cursor: help;
}

/* An alternative box on the page. Dashed, one ink, and never a status hue: it is
   not a placement this run makes, so it must not wear the vocabulary the legend
   assigns to placements this run does make. Drawn only for the SELECTED unit. */
.alt-overlay {
  border: 2px dashed var(--placement-ink);
  border-radius: 2px;
  background: transparent;
  cursor: pointer;
  opacity: 0.75;
}

.alt-overlay[data-active] {
  border-color: var(--placement-active);
  border-style: solid;
  opacity: 1;
}

.alt-overlay:hover,
.alt-overlay:focus-visible {
  opacity: 1;
  outline: 2px solid var(--placement-active);
  outline-offset: 1px;
}

/* The spotlight fades everything that is not the selection. An alternative box
   belongs to the selection -- it is the same unit, placed elsewhere -- so it is
   exempt, or choosing one would dim the thing just chosen. */
body.has-selection .alt-overlay {
  opacity: 1;
}

/* ---- compare.html: one work's printed editions side by side (#152) ----- */

/* A THIRD PAGE, so a third body layout, and it is deliberately not a variant of
   `body.viewer`: that one is a two-pane split with a draggable splitter and one
   image, this one is a reading column plus N image columns. What the two share
   is the region overlay vocabulary above (`.region-overlay` and every rule that
   selects on it), which is why nothing here restates it. */
body.compare {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* In the flow rather than over an image: on this page the notice is about the
   REQUEST (a `?text=` no run of the work carries), not about one column, so it
   has nowhere in particular to sit and belongs across the top. Same colour as
   `.canvas-notice`, because it is the same kind of statement. */
.compare-notice {
  flex: 0 0 auto;
  padding: 8px 12px;
  background: #7a2e1e;
  color: #fdf3ee;
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
}

.compare-notice[hidden] {
  display: none;
}

/* The unit switcher borrows the canvas switcher's buttons: it is the same
   control one level up — the page turner of a comparison is the text unit,
   because the columns share a unit and share no page. */
.unit-switch #unit-label {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  min-width: 12ch;
  text-align: center;
}

main.compare-main {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
}

/* ---- The e-text column ------------------------------------------------ */

/* Its OWN column, not a property of any edition: it is the axis every edition
   is aligned against, and the reason a comparison is possible at all. */
/* THE LIST SCROLLS, NOT THE PANE. The headline and the selected verse are the
   two things a reader is comparing against, and an early draft let the whole
   column scroll: choosing a unit two thousand rows down scrolled both of them
   off the top, so the pivot the columns are aligned on was the one thing not on
   screen. */
/* WIDENED WHEN THE ROWS GAINED THEIR TEXT. 380px held a column of identifiers
   comfortably; it holds about 55 characters of Latin transliteration at the
   row's 13.5px, which wraps a long pāda twice. 460px is nearer the measure
   running text wants, and it still leaves 980px at a 1440px viewport — two
   edition columns above their 300px minimum.

   ~~which is the widest case the site has apart from the Maitrāyaṇī's four~~ — NO
   LONGER TRUE, and measured rather than reasoned since 2026-09-09: the
   Ṛgveda has THREE `edition_id`s — Aufrecht, Müller's first edition over six runs and
   his second. (The Taittirīya-Brāhmaṇa briefly had three too, for the wrong reason:
   matching #247 aligned every id to the EDITION and it is two of three runs each.) At
   1440px
   the pane takes 432px and the strip 1008px, and three columns fit because only the
   ones in `data-state="placed"` claim 300px — on that work's first paint two do and
   the third stands on a sentence at 230px, by the rule further down. Pinned by
   `tests/browser/compare-smoke.test.js`, which also checks that at 320px the STRIP
   scrolls and the document does not. */
#etext-pane {
  flex: 0 0 clamp(260px, 30%, 460px);
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 10px 10px 0;
  background: var(--bg);
  border-right: 1px solid var(--line);
}

.etext-summary,
.etext-current {
  flex: 0 0 auto;
}

.etext-headline {
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

/* The sentence that says whether these columns can be read against each other at
   all. Body text, never a tooltip: on three of the six multi-run works the site
   publishes the answer is no, and a heading that promises otherwise while the
   truth hides behind a hover is the page telling a reader something untrue. */
.etext-headnote {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}

/* Never a status hue: a bundle that could not be read, and two editions
   disagreeing about a verse's text, are not points on the matched / partial /
   ambiguous / unmatched scale. */
.etext-warning {
  margin-top: 6px;
  padding: 6px 8px;
  border-left: 3px solid var(--verify-refuted);
  background: var(--panel-bg);
  font-size: 12px;
  line-height: 1.6;
}

.etext-current {
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}

.etext-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  color: var(--muted);
}

.etext-verse {
  margin: 4px 0 8px;
  font-size: 15px;
  line-height: 1.7;
}

/* The selected unit's bundle carries it with an empty text. It takes the
   verse's slot and NOT the verse's size: a box of nothing at 15px is the blank
   line of verse this must never be, and the sentence in it is ours, not a
   reading. */
.etext-verse-absent {
  margin: 4px 0 8px;
  font-size: 12px;
  line-height: 1.6;
  font-style: italic;
  color: var(--muted);
}

.etext-source,
.etext-rights,
.etext-missing {
  font-size: 11px;
  line-height: 1.6;
  color: var(--muted);
}

/* A declared absence standing where the verse would be. It borrows
   `.absence-marker`'s treatment for the reason that treatment exists: this is
   the same fact the text list marks, in the column a reader looks to for the
   text. It is NOT set in the verse's size — there is no verse here. */
.etext-absence-heading {
  margin: 4px 0 2px;
  padding-left: 8px;
  border-left: 3px solid var(--verify-refuted);
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.6;
}

.etext-absence-detail {
  padding-left: 8px;
  border-left: 3px solid var(--verify-refuted);
  margin-bottom: 4px;
  font-size: 12px;
  line-height: 1.7;
  color: var(--muted);
}

.unit-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  list-style: none;
  margin: 8px -10px 0;
  padding: 0 10px 18px;
}

/* A ROW IS A LINE OF TEXT WITH ITS CITATION OVER IT, not a citation on its own.
   The list held `text_id` and a count and no text at all, so the pivot column —
   the one thing every edition is aligned against — was a list of identifiers a
   reader could step through but not READ. The row is now two lines: the id and
   its edition count in the small muted monospace above, the text at reading size
   below. Stacked rather than side by side so that every verse in the column
   starts at the SAME left edge; an id inline before the text would jog that edge
   by up to seventeen characters (`YVW_SBM_1_1_1_1_a`) from row to row, and a
   column of text that does not line up is a table. */
.unit-pick {
  display: flex;
  flex-direction: column;
  width: 100%;
  align-items: stretch;
  gap: 1px;
  padding: 4px 6px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

/* The citation line: id, how many editions, and the disagreement mark if there
   is one. It sits above the text and is deliberately quieter than it. */
.unit-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}

/* WHAT THE EYE LANDS ON. Full contrast against the id's `--muted`, and the
   verse's line height rather than the chrome's, because this is the text. It
   wraps rather than being clipped: a truncated verse is a verse a reader cannot
   read, which is the state this whole change exists to leave. */
.unit-text {
  font-size: 13.5px;
  line-height: 1.65;
  overflow-wrap: anywhere;
}

/* A unit whose bundle carries no text. NOT the verse treatment — smaller,
   muted, italic — because it is our sentence about the data and not a reading of
   it, and above all not a blank, which would read as an empty line of verse. */
.unit-text-absent {
  font-size: 11.5px;
  line-height: 1.6;
  font-style: italic;
  color: var(--muted);
}

/* THE SEAM ROW, and it is deliberately NOT a `.unit-row` (#156). It carries no
   button, no id a URL can select and no `data-text-id`, because there is no unit
   here — a selectable placeholder for a passage the source text does not contain
   is the substitution this mechanism exists to refuse, the same reason
   `.absence-marker` is not a `.text-card` in viewer.html.

   It borrows that marker's dashed `--verify-refuted` treatment so the two
   surfaces read as one statement, and it is quieter than the card's because a
   list row has less room and sits between two readable verses. */
.unit-absence {
  border: 1px dashed var(--verify-refuted);
  border-radius: 8px;
  padding: 7px 10px;
  margin: 6px 0;
  background: color-mix(in srgb, var(--verify-refuted) 6%, transparent);
  cursor: default;
}

.unit-absence-citation {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--verify-refuted);
}

.unit-absence-reason,
.unit-absence-ids {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--muted);
}

/* The ids are data and wrap as data: a clipped id list is a list a reader cannot
   check against a citation. */
.unit-absence-ids {
  overflow-wrap: anywhere;
}

.unit-pick:hover {
  background: var(--preview-bg);
}

/* The selected row is the one the columns are standing on. Only the citation
   line takes the bold: setting the verse itself bold would change how the text
   READS depending on where the cursor is. */
.unit-row.selected .unit-pick {
  background: var(--selected-bg);
}

.unit-row.selected .unit-head {
  font-weight: 700;
}

.unit-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Two editions under one id with different text in them. It borrows the refuted
   red the e-text warning uses for the same reason that warning does: it is not a
   point on the matched / partial / ambiguous / unmatched scale, and one row of
   text standing for two editions that disagree is exactly the substitution this
   page refuses. Visible without a hover; which runs, and which one is shown, is
   the title. */
.unit-disagrees {
  flex: 0 0 auto;
  padding: 0 5px;
  border-radius: 8px;
  border: 1px solid var(--verify-refuted);
  color: var(--verify-refuted);
  font-size: 10px;
  white-space: nowrap;
}

/* How many EDITIONS carry this unit (issue #155 — it counted runs, and two
   volumes of one edition never share a unit, so on five of six multi-run works it
   counted nothing, as the site stood on 2026-09-03). It rides at the end of the citation line now that the text
   has the row's second line, and it still does its own job: on the Ṛgveda it
   stops where Müller's first volume does, so a reader can see the coverage
   difference before clicking anything. Which volume is the badge's title. */
.unit-runs {
  flex: 0 0 auto;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 8px;
  background: var(--line);
  color: var(--muted);
  font-size: 11px;
  text-align: center;
}

.unit-row[data-shared] .unit-runs {
  background: var(--accent);
  color: #fff;
}

/* A PART THE PAGE HAS NOT FETCHED, keeping its place in the reading list. It is
   not a unit and is not dressed as one: it is a rule with the part's range on it,
   so a reader scrolling out of kāṇḍa 1 sees kāṇḍa 2 rather than the end of the
   work. Stepping onto it fetches that bundle. */
.part-row {
  margin: 6px 0;
}

.part-pick {
  display: flex;
  width: 100%;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 6px;
  border: 1px dashed var(--line);
  border-radius: 4px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}

.part-pick:hover {
  background: var(--preview-bg);
}

.part-range {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 700;
}

.part-note {
  flex: 0 1 auto;
  color: var(--muted);
  font-size: 11px;
  text-align: right;
}

/* ---- The edition columns ---------------------------------------------- */

/* Unbounded in principle, but no longer unbounded in practice: one column per
   EDITION (issue #155) makes the widest work on the site four — Schroeder's four
   Maitrāyaṇī volumes, which declare four edition ids — where counting runs made
   the Śatapatha sixteen. The row still SCROLLS rather than crushing every column
   past legibility, and a column with no image takes a narrower track than one
   with a page in it. */
.edition-columns {
  flex: 1 1 auto;
  display: flex;
  gap: 1px;
  min-width: 0;
  min-height: 0;
  overflow-x: auto;
  background: var(--line);
}

.edition-column {
  flex: 1 1 0;
  min-width: 300px;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--panel-bg);
}

/* A column standing on a stated absence is a sentence, not a page: it does not
   need a page's width. It is never hidden and never narrower than its sentence.
   It mattered most when a column was a run and the Śatapatha stood in fourteen
   absences at once; with columns as editions the worst case is one leaf beside
   three sentences, and the rule still earns its place there. */
.edition-column:not([data-state="placed"]) {
  flex: 0 1 230px;
  min-width: 190px;
}

/* With no image competing for the head, the open part's own range can have the
   room to be read whole -- and on a work of volumes that string (`YVW_SBM_2`) is
   the single most useful thing in the column, because it says which part of the
   work is open. */
.edition-column:not([data-state="placed"]) .column-range {
  overflow: visible;
  white-space: normal;
}

.column-head {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
}

.column-name {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}

/* The column's identity is the EDITION (issue #155): `sbm-weber-1` stands over
   fifteen bundles and `sbm-chowkhamba-1` over one. Which of an edition's parts is
   open is the line below, not this one.

   IT IS STILL BOLD, but it is no longer the first thing on the line, because it is
   not the book's name (issue #185): `rv-mueller-1` beside `rv-mueller-1890` read
   as two Müller volumes rather than as two editions. The caption in front of it is
   what says it is an identifier. */
.column-edition {
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
}

/* The caption on the id. Deliberately the quietest thing in the head — smaller
   and muted — so it frames the slug without competing with it for the reader's
   eye. It is chrome and is translated; the slug beside it is data and is not (see
   the note at the top of assets/js/strings.js).

   AND NO `text-transform`. An earlier draft set `uppercase` on both captions and
   the label caption substitutes a RUN ID, so the rendered text read
   `AS RV-V1 RECORDS IT` — a presentational rule rewriting a data value, which is
   the same mistake as translating one. Neither caption transforms its text, so
   the pair also stays visually consistent. */
.column-caption {
  flex: 0 0 auto;
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--muted);
  white-space: nowrap;
}

/* And the caption over the LABEL, which is the book's actual name. Without it the
   label is an unlabelled wall of prose below an identifier, which is how the
   authoritative name managed to be present in the DOM and absent from the
   reader's hands. Full width, immediately above the box it names. */
.column-label-caption {
  flex: 1 0 100%;
  margin-top: 3px;
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--muted);
}

.column-range {
  font-size: 12px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.column-open {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}

/* The edition's own label — the book's bibliographic name and the caveats the
   recipe attached to it. VISIBLE, not a tooltip, and not truncated: `rv-m1`'s
   runs to some two thousand characters whose second half is what a reader most
   needs, so it is bounded and scrolls instead of being cut at a full stop. It
   takes the whole width of the head, below the id and the link. */
.column-label {
  flex: 1 0 100%;
  max-height: 4.6em;
  overflow-y: auto;
  margin-top: 1px;
  font-size: 11px;
  line-height: 1.55;
  color: var(--muted);
}

/* WHICH VOLUME, which is half of the answer selecting a passage owes. Only shown
   for an edition published as more than one run — there is nothing to say about
   the parts of a one-part edition. It takes the whole width below the id, so the
   run it names is never truncated. */
.column-part {
  flex: 1 0 100%;
  margin-top: 2px;
  font-size: 11px;
  color: var(--muted);
}

.column-open:hover {
  text-decoration: underline;
}

.column-meta {
  flex: 0 0 auto;
  padding: 6px 10px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

.column-page {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.column-osd {
  flex: 1 1 auto;
  position: relative;
  min-height: 220px;
  background: #20201c;
}

.column-osd[hidden] {
  display: none;
}

.column-foot {
  flex: 0 0 auto;
  padding: 5px 10px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  line-height: 1.5;
  color: var(--muted);
}

/* ---- Where an image would be, when there is not one -------------------- */

/* FOUR STATES AND FOUR SENTENCES, never an empty frame. The declared absence
   borrows `.absence-marker`'s treatment because it is the same fact in another
   place — a passage the SOURCE TEXT does not have, declared by the run's recipe
   and checked by its build. The two the viewer merely OBSERVES (a unit carried
   with no box, a unit this bundle does not carry) are quieter, because they
   claim less: no reason is stated for either, and none may be. The unreadable
   one is loud and is not an absence at all — it is a fact about this session. */
.column-state {
  margin: 4px 0 8px;
  padding: 10px 12px;
  border: 1px dashed var(--muted);
  border-radius: 6px;
  background: var(--bg);
}

.column-state[data-state="declared-absent"] {
  border-color: var(--verify-refuted);
}

.column-state[data-state="unreadable"] {
  border-style: solid;
  border-color: #7a2e1e;
  background: #fdf3ee;
}

.column-state-heading {
  font-size: 13px;
  font-weight: 700;
  line-height: 1.6;
  color: var(--ink);
}

.column-state-detail {
  margin-top: 4px;
  font-size: 11.5px;
  line-height: 1.7;
  color: var(--muted);
}

/* ---- Narrow screens ---------------------------------------------------- */

/* The work's title stays at EVERY width on this page. `viewer.html` sheds
   `.edition` at 700px because its run selector still names the run there; this
   page has no such fallback, and the title is the only thing on screen that says
   which work the columns are of. There is room for it: this header carries no
   run selector, no highlight switch and no legend. */
@media (max-width: 700px) {
  body.compare header.app-bar .edition {
    display: block;
  }
}

/* Columns become a STACK, not a horizontal scroll: a column pushed off the side
   of a phone is a column a reader cannot find, which is the silent blank this
   page exists to refuse, one step removed. The e-text keeps the top. */
@media (max-width: 900px) {
  main.compare-main {
    flex-direction: column;
    overflow-y: auto;
  }

  #etext-pane {
    flex: 0 0 auto;
    max-height: 45vh;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .edition-columns {
    flex-direction: column;
    overflow-x: visible;
  }

  .edition-column,
  .edition-column:not([data-state="placed"]) {
    flex: 0 0 auto;
    min-width: 0;
  }

  .column-osd {
    min-height: 65vh;
  }
}
