/* 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 one warm alarm colour: absent passages, unreadable models and
     conflicting ids. It marks no match status. */
  --verify-refuted: #b3261e;

  --bg: var(--veda-color-bg);
  --panel-bg: var(--veda-color-surface);
  --ink: var(--veda-color-text);
  --muted: var(--veda-color-text-muted);
  --line: var(--veda-color-border-subtle);
  --accent: var(--veda-color-action);
  --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;

  /* Overlay dimensions, shared by the image overlays and the header legend so a
     swatch is drawn exactly as the mark on the page. Lines are CSS pixels, not
     image pixels, so they stay hairlines at any zoom and never cover the script. */
  --overlay-line: 1px;
  --overlay-double: 3px; /* 1px line, 1px gap, 1px line */
  --overlay-alpha: 65%; /* resting status lines only; hover/selected go to 100% */
  /* The page image is light in both themes, but the dark theme swaps in pale status
     colours made for a dark UI; a 1px pale line on cream all but vanishes. Marks on
     the image are therefore mixed toward black there (hue family kept, still
     model-driven); the header legend, which sits on the UI, is not. */
  --overlay-ink: 0%;
  --overlay-brightness: 1;
  --overlay-preview-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 85%, transparent);
  --overlay-selected-shadow:
    inset 0 0 0 1px rgba(18, 18, 18, 0.92), 0 0 0 1px rgba(255, 255, 255, 0.95);

  font-family: var(--veda-font-sans);
}

* {
  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 {
  min-height: 52px;
  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: 0.9375rem;
  font-weight: 700;
  margin: 0;
  white-space: nowrap;
}

/* The work title and the edition's short name (display-names.js). Short by
   construction, so it wraps rather than being cut with an ellipsis (quiet UI). */
header.app-bar .edition {
  font-size: 0.875rem;
  color: var(--muted);
  /* Never cut: the decorative legend gives way first. It shrinks only when it
     alone is wider than the bar, which a catalog name never is but the reader's
     own file names (local mode) can be; then it wraps, long tokens included,
     rather than widening the page. */
  flex: 0 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
header.app-bar .edition .edition-name {
  color: var(--ink);
  font-weight: 600;
}
header.app-bar .edition .veda-badge {
  display: inline-block;
}

/* 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: 0.875rem;
}

/* 230px -> 200px: the shared 44px neighbour buttons cost 32px of the desktop
   row, and the selector is the only slack on it (cap ladder 200/150/130/120). */
.run-switch select {
  inline-size: auto;
  max-width: 200px;
  min-inline-size: 0;
}

/* Catalog neighbor navigation (issue #57): buttons that step to the previous/next
   catalogued run. They are shared `.veda-button`s (size, hover, focus, disabled);
   only the seam marker below is service-specific. 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. */
/* 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);
}

/* The reading-MODE switch, 単一 / 並列, in both app bars. Deliberately
   a marked current option and an anchor for the other, because it answers
   "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: 0.875rem;
  white-space: nowrap;
}

/* Width fit only: the anchors are shared quiet buttons (44px target, focus,
   hover); the narrower inline padding is what keeps the app bar on one row. */
.app-bar .veda-button--quiet {
  padding-inline: var(--veda-space-2);
}

.mode-switch .current,
.mode-switch .unavailable {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--veda-target-min);
  padding-inline: var(--veda-space-1);
}

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

.mode-switch .unavailable {
  color: var(--veda-color-text-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-flex;
}

/* The text switch (GRETIL / TITUS / DCS). Unlike the mode switch, the current
   entry is never shed at narrow widths: it is the one thing that says which text
   the cards show. */
.text-source-switch {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  font-size: 0.875rem;
  white-space: nowrap;
}
.text-source-switch[hidden] {
  display: none;
}
.text-source-switch .label {
  color: var(--veda-color-text-muted);
}
.text-source-switch .current {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--veda-target-min);
  padding-inline: var(--veda-space-1);
  color: var(--veda-color-text);
  font-weight: 700;
}
.text-source-switch .sep {
  color: var(--line);
}

/* What the text on screen is when it is not the print's (a DCS analytic text),
   and an unhonoured `?variant=`: a band under the app bar, outside both panes so
   it stays in view whichever pane a narrow screen shows. It wraps, and never
   pushes the page wider than the screen. */
.text-source-notes {
  flex: 0 0 auto;
  min-width: 0;
  padding: 4px 12px;
  border-bottom: 1px solid var(--line);
  color: var(--veda-color-text-muted);
  font-size: 0.8125rem;
  line-height: 1.45;
}
.text-source-notes[hidden] {
  display: none;
}
.text-source-note {
  margin: 0;
  overflow-wrap: anywhere;
}
.text-source-note--unavailable {
  color: var(--veda-color-text);
}
.rights-text-note {
  margin: 8px 0 0;
}

.canvas-switch {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.875rem;
  /* The page switch is one unit: the bar may wrap AROUND it, never squeeze it. */
  flex: 0 0 auto;
}

/* `p.983 (27/52)` stays on one line; the bar wraps between groups instead. */
#canvas-label {
  white-space: nowrap;
}

/* The two arrows keep a 44 CSS px square target, but do not grow with the
   reader's text size: at 200% text the token's rem floor would make each 88px
   wide and push the switch (label included) past a 320px phone. Height still
   follows the token. */
.canvas-switch button {
  min-inline-size: min(var(--veda-target-min), 44px);
  padding-inline: var(--veda-space-1);
}

/* The overlay highlight is viewer-specific, so it stays in the bar rather than
   in the shared language / colour popovers. */
.highlight-switch {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  font-size: 0.875rem;
  white-space: nowrap;
}

.highlight-switch select {
  inline-size: auto;
}

body.viewer .highlight-switch {
  margin-left: auto;
}

.legend {
  display: flex;
  gap: 7px;
  font-size: 0.875rem;
  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: var(--overlay-line) solid
    color-mix(in srgb, var(--swatch-color, var(--status-unmatched)) var(--overlay-alpha), transparent);
  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: var(--overlay-double); /* 1px line, 1px gap, 1px line */
}

/* 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: var(--overlay-selected-shadow);
}
.legend i.legend-ring.preview {
  box-shadow: var(--overlay-preview-shadow);
}

/* 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: var(--overlay-line);
  border-bottom: none;
  background-image: var(--group-rule-image);
  opacity: var(--overlay-alpha);
}

/* 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;
  }
  /* 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;
  }
  html[lang="en"] .highlight-switch .label {
    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 highlight mode is the only control for it, so it never sheds: the bar
   wraps instead (see the flex-wrap rule on the viewer/compare app bar). */

/* 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;
  }
  /* The edition's short name and any "view only" status stay at every width
     (the selector shows only the run's part); the work title gives way. */
  body.viewer header.app-bar .edition .work-title {
    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;
  }
  /* 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;
  }
}


/* 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;
  }
  /* The text switch's caption ("Text" / 本文) and its `/` give way, never its two
     entries: the nav keeps the caption as its accessible name, and the current
     text is still the bold, unlinked one. Without them the switch fits beside the
     page-turn controls instead of taking a row of its own, which at 320px left the
     viewer split 233px of the 280px the header budget promises (measured on
     veda-ai's first run, rv-v1, a text variant since issue #276). */
  .text-source-switch .label,
  .text-source-switch .sep {
    display: none;
  }
  .text-source-switch {
    gap: 2px;
  }
  .text-source-switch .current {
    padding-inline: 2px;
  }
  /* A THREE-way text switch (GRETIL / TITUS / DCS, veda-ai) does not fit beside the
     page-turn controls even so, and takes a row of its own; then, in English, the
     highlight selector (141px: its width is its longest option, "Selected only")
     no longer fits beside the page-turn controls either and took a fourth row,
     leaving the viewer split 233px of the 280px the header budget promises
     (measured at 320px on veda-ai's first run, rv-v1, which has that switch since
     issue #293; ms-k1 had the same shape). Capped like the run selector above, it
     rides beside the page-turn controls again; the closed selector shows its
     current option, and "All", the default, is whole. Japanese (102px) fits. */
  html[lang="en"] .highlight-switch select {
    max-width: 126px;
  }
}

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

.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;
  border-radius: 0;
  font-size: 0.875rem;
  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: 4px 12px;
  background: var(--veda-color-bg);
  color: var(--veda-color-text-muted);
  font-size: 0.875rem;
  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: 0.875rem;
  line-height: 1.7;
}

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

.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 translation pane (issue #157) --------------------------------- */

/* A third column, only where a companion is offered (`body[data-translation]`);
   every other run keeps the two panes above exactly as they were. */
#translation-pane[hidden] {
  display: none;
}
body[data-translation] #image-pane {
  flex-basis: 42%;
}
body[data-translation] #text-pane {
  flex-basis: 30%;
}
#translation-pane {
  flex: 1 1 28%;
  min-width: 220px;
  overflow-y: auto;
  padding: 12px 12px 18px;
  background: var(--bg);
  border-inline-start: 1px solid var(--line);
}
.translation-head h2 {
  margin: 0 0 4px;
  font-size: 1rem;
}
.translation-citation,
.translation-status,
.translation-members,
.translation-qual,
.translation-pages {
  margin: 4px 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--veda-color-text-muted);
}
.translation-status {
  color: var(--veda-color-text);
}
.translation-review {
  margin: 6px 0;
  font-size: 0.875rem;
}
.translation-source {
  margin: 6px 0 10px;
  font-size: 0.875rem;
}
.translation-source p {
  margin: 4px 0;
  overflow-wrap: anywhere;
}
.translation-block {
  margin: 0 0 10px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--veda-radius-sm);
  background: var(--veda-color-surface);
}
.translation-block.selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent);
}
.translation-ref {
  margin: 0 0 4px;
  font-size: 0.9375rem;
}
.translation-coverage {
  font-size: 0.875rem;
  font-weight: normal;
  color: var(--veda-color-text-muted);
}
.translation-body {
  cursor: pointer;
}
.translation-body:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.translation-text {
  margin: 0;
  white-space: pre-line;
  line-height: 1.6;
}
.translation-pages .veda-button {
  margin-inline-end: 4px;
}
/* Griffith's own printed page (issue #310): a row of its own under the stanza,
   apart from the ORIGINAL's page buttons above it. */
.translation-printed {
  margin: 6px 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--veda-color-text-muted);
}
.translation-printed-open {
  margin-inline-end: 4px;
}

/* The page dialog: wider than the rights dialog, its own deep-zoom viewer sized
   to the screen, and the whole body scrolling on a phone. */
dialog.translation-page-dialog {
  max-width: 760px;
  width: calc(100% - 16px);
  max-height: calc(100dvh - 16px);
}
dialog.translation-page-dialog[open] {
  display: flex;
  flex-direction: column;
}
dialog.translation-page-dialog .head {
  gap: 8px;
}
dialog.translation-page-dialog .body {
  overflow-y: auto;
  min-height: 0;
}
dialog.translation-page-dialog .body p {
  margin: 4px 0;
  overflow-wrap: anywhere;
}
.translation-page-osd {
  position: relative;
  width: 100%;
  height: clamp(240px, 58dvh, 640px);
  margin: 8px 0;
  background: var(--veda-color-surface-subtle);
  border: 1px solid var(--line);
}
.translation-page-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 6px 0;
}
.translation-page-nav [aria-pressed="true"],
.translation-page-evidence [aria-pressed="true"],
.translation-page-stanzas [aria-current="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}
.translation-page-citation {
  font-weight: 600;
}
.translation-page-stanzas {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin: 4px 0;
}
.translation-page-stanzas > span {
  flex: 0 0 auto;
  margin-inline-end: 4px;
}
.translation-page-stanzas .veda-button {
  margin: 0;
}
/* Phone: the label takes its own row, so wrapped links start flush with it
   instead of sitting under the label's text at the button's padding offset. */
@media (max-width: 480px) {
  .translation-page-stanzas > span {
    flex: 1 0 100%;
  }
}
.translation-page-notice {
  margin: 0;
  padding: 6px 12px;
  border-top: 1px solid var(--line);
  font-size: 0.85em;
  color: var(--veda-color-text-muted);
}
.translation-page-evidence {
  margin: 4px 0;
  padding-inline-start: 1.2em;
}
.translation-page-evidence li {
  margin: 2px 0;
}
.translation-page-status:empty {
  display: none;
}
.translation-page-credit {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
  color: var(--veda-color-text-muted);
}

/* An original card that IS a member of the selected stanza: a second rule beside
   the selection ring, so a line-mate selected only for the image's sake (an image
   line shared with the next stanza) is not mistaken for a member. */
.text-card.translation-member {
  box-shadow: inset 4px 0 0 var(--accent);
}

/* ---- 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: var(--veda-radius-sm);
  padding: 9px 12px;
  margin-bottom: 9px;
  cursor: default;
  background: color-mix(in srgb, var(--verify-refuted) 6%, transparent);
}

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

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

/* Chapters a DCS text does not have (#315): the same fact as an absence, said
   from the DCS listing, so the same treatment; not a `.text-card`. */
.dcs-missing-marker {
  border: 1px dashed var(--verify-refuted);
  border-radius: var(--veda-radius-sm);
  padding: 9px 12px;
  margin-bottom: 9px;
  cursor: default;
  background: color-mix(in srgb, var(--verify-refuted) 6%, transparent);
}

.dcs-missing-marker--locus {
  outline: 2px solid var(--verify-refuted);
  outline-offset: 1px;
}

.dcs-missing-marker .dcs-missing-heading {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--verify-refuted);
}

.dcs-missing-marker .dcs-missing-detail {
  margin-top: 4px;
  font-size: 0.875rem;
  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: var(--veda-radius-sm);
  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: 0.875rem;
  color: var(--muted);
}

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

/* The match status as a small mark: its word in the meta row's ink, behind a
   short rule in the status colour (the card's left edge carries the same
   colour), so the word, not the hue, is what tells the states apart. */
.badge {
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--ink);
  white-space: nowrap;
}

.badge::before {
  content: "";
  display: inline-block;
  inline-size: 0.75em;
  block-size: 3px;
  margin-inline-end: 4px;
  vertical-align: middle;
  background: var(--status-unmatched);
}

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

/* QUIET CARDS. At rest a card is its id and its text. The default state -- a
   match -- says nothing; only an exception (another match status) shows its
   mark. The normalized text (and, for the reader's own text, its source line)
   belong to the card being read, so they appear on the selected card only, with
   every mark it carries. A published card never names a file.
   A card with no box cannot be selected (`.selectable` is set only where one
   can), so it is never folded: it shows at rest what a selected card shows. */
.text-card.selectable:not(.selected) .badge[data-status="matched"],
.text-card.selectable:not(.selected) .src,
.text-card.selectable:not(.selected) > details {
  display: none;
}

.text-card .verse {
  font-size: 0.9375rem;
  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: 0.875rem;
  line-height: 1.6;
  font-style: italic;
  color: var(--muted);
}

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

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

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

.text-card details .norm {
  margin-top: 4px;
  font-family: var(--veda-font-mono);
  font-size: 0.875rem;
  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: 0.875rem;
}

.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: 0.875rem;
  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: var(--overlay-line);
  /* Only the drawn line is translucent, never the whole overlay (the spotlight
     owns opacity), so focus and selection are not weakened with it. */
  --rule-alpha: var(--overlay-alpha);
  --rule-color: color-mix(
    in srgb,
    color-mix(in srgb, currentColor calc(100% - var(--overlay-ink)), #000)
      var(--rule-alpha),
    transparent
  );
  border-radius: 0;
  /* 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: 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 var(--rule-color);
}

/* 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: var(--overlay-double); /* `double` needs 3px: line, gap, line */
}

/* 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-10-27 (issue #179): 54 bundles, 91,178 regions,
   every one on a `matched` unit; 0 `partial`, 0 `ambiguous`, and 59,194 `unmatched`
   units that carry no box by construction. 22,947 of the 59,903 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);
  /* The gradient names var(--status-*), not currentColor, so the alpha that
     --rule-color gives the single-status rule is applied here as opacity. */
  opacity: var(--rule-alpha);
  filter: brightness(var(--overlay-brightness));
}

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

/* ...and the mixed gradient does not follow `color`, so hide it explicitly. */
body[data-highlight="selected"]
  .region-overlay:not(.selected):not(.preview)::after {
  opacity: 0;
}

/* 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 2,784 regions on the 528 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] {
  /* One hairline all round, and the bottom edge belongs to the ::after rule
     alone, so the two never stack into a darker, thicker line there. */
  border: var(--overlay-line) solid var(--rule-color);
  border-bottom: 0;
}

.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. */

/* 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-alpha: 100%;
  box-shadow: inset 0 0 0 1px
    color-mix(
      in srgb,
      color-mix(in srgb, var(--accent) calc(100% - var(--overlay-ink)), #000) 85%,
      transparent
    );
}

/* Selection is hue-free: a 1px near-black keyline inside the box and a 1px white
   one just outside it, so it reads on parchment, ink and any status colour
   without painting the script. The status line keeps its own style (solid,
   dashed, dotted, double) at full strength, and there is no fill and no pulse. */
.region-overlay.selected {
  z-index: 5;
  --rule-alpha: 100%;
  box-shadow: var(--overlay-selected-shadow);
}

@media (prefers-reduced-motion: reduce) {
  .region-overlay {
    transition: 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 {
  min-height: 40px;
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  padding: 4px 14px;
  background: var(--panel-bg);
  border-top: 1px solid var(--line);
  font-size: 0.875rem;
  color: var(--muted);
}

footer.rights-bar .nc {
  /* The non-commercial condition is a legal requirement, shown as text (not a
     badge, quiet UI Q6): it must never shrink or wrap. */
  flex: 0 0 auto;
  white-space: nowrap;
}

footer.rights-bar .grow {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
/* One line of source: the bar keeps the licence links and the non-commercial
   condition; the attributions are in full one click away ("Rights"). */
footer.rights-bar .grow .attribution {
  display: none;
}
/* One quiet sentence that may wrap; it never hides or squeezes the licences. */
footer.rights-bar .correspondence-notice {
  flex: 1 1 16em;
  min-width: 0;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
/* Phones: the footer reads as three groups, each starting on a row of its own
   when it does not fit beside the one before — the rights (non-commercial
   condition + licences), the notice, and the actions (rights dialog, report,
   project link). Nothing is hidden or shrunk: every piece keeps the width of the
   row, and the reading area is spared the rows a half-filled line used to cost. */
@media (max-width: 700px) {
  footer.rights-bar {
    row-gap: 0;
  }
  /* The licences share the non-commercial line instead of taking their own. */
  footer.rights-bar .grow {
    flex: 1 1 calc(100% - 8em);
  }
  /* A full row: sharing one with the report link squeezed it to four lines. */
  footer.rights-bar .correspondence-notice {
    flex-basis: 100%;
  }
  /* Run-scope links may wrap on a phone rather than push the footer sideways. */
  footer.rights-bar .external-links {
    flex: 1 1 auto;
    min-width: 0;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}
footer.rights-bar a {
  color: var(--accent);
}

footer.rights-bar button {
  flex: 0 0 auto;
  white-space: nowrap;
}

dialog.rights-detail {
  border: 1px solid var(--line);
  border-radius: var(--veda-radius-sm);
  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: 0.9375rem;
  margin: 0;
  flex: 1 1 auto;
}

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

dialog.rights-detail h3 {
  font-size: 0.875rem;
  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);
}


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

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

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

body.index .page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 16px;
}

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

/* One run = one card: the only framed group on the page (quiet UI Q5). Work and
   edition groups are headings and spacing, never a frame around the cards. */
body.index .run-list .run-item {
  gap: var(--veda-space-1);
  margin-bottom: var(--veda-space-3);
}

/* 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: 0.9375rem;
  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 " · " between the name and the count is
   real text in the DOM (assets/js/index.js), so it reaches a screen reader. */
body.index .edition-heading {
  font-size: 0.875rem;
  font-weight: 400;
  margin: 0 0 6px;
  color: var(--muted);
}

/* The edition's short name (assets/js/display-names.js). */
body.index .edition-heading .edition-name {
  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: 0.875rem;
  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: 0.875rem;
  color: var(--muted);
}

/* ---- index.html: the catalog overview --------------------------------- */
/* The page's primary discovery surface (assets/js/index.js): a heading per text
   class that holds an edition, and under it a line per Veda with its works. Only
   filled places are drawn, so the block is as tall as the catalog is full and
   reads the same on a desktop and a phone — there is no table to reflow. */
body.index .veda-grid-wrap {
  margin: 20px 0 28px;
}

/* The page's two section headings (h2): the overview 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: 1rem;
  font-weight: 600;
  margin: 0 0 10px;
}

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

body.index .stratum-group {
  padding: 8px 0;
  border-top: 1px solid var(--line);
}
body.index .stratum-name {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--muted);
  margin: 0 0 4px;
}

body.index .veda-lines {
  list-style: none;
  margin: 0;
  padding: 0;
}
/* Veda label and its works on one line, wrapping to two when the pair is too wide
   for the viewport rather than clipping or scrolling. The label column is as wide
   as the widest label, `Yajurveda (White)`, so the works line up. */
body.index .veda-lines > li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 12px;
  padding: 3px 0;
  font-size: 0.875rem;
}
body.index .veda-name {
  flex: 0 0 auto;
  min-width: 9em;
  color: var(--muted);
}

body.index .cell-works {
  flex: 1 1 14em;
  min-width: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow-wrap: anywhere;
}
body.index .cell-works a {
  font-weight: 600;
  text-decoration: none;
}
body.index .cell-works a:hover,
body.index .cell-works a:focus-visible {
  text-decoration: underline;
}
/* The two numbers after a work (issue #183): how many EDITIONS of it this site
   publishes, and how many runs those are published as, which is what the link
   lands on. */
body.index .cell-works .cell-counts {
  margin-left: 6px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

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

/* 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: 0.875rem;
  color: var(--muted);
  padding: 0 2px;
}

/* Run cards rendered from the catalog (assets/js/index.js): the short name is the
   link; id, counts and rights are plain metadata lines. */
body.index .run-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
}
body.index .run-head .run-name {
  font-weight: 700;
}
body.index .run-item p {
  margin: 0;
  font-size: 0.875rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}
body.index .run-meta {
  font-variant-numeric: tabular-nums;
}
body.index .run-details {
  font-size: 0.875rem;
}
body.index .run-details > summary {
  inline-size: fit-content;
  min-block-size: 2.75rem;
  display: flex;
  align-items: center;
  color: var(--veda-color-text-muted);
  cursor: pointer;
}

/* The door to local mode: a heading, one sentence and a button, no frame. */
body.index .local-entry {
  margin: var(--veda-space-5) 0;
}
body.index .local-entry h2 {
  font-size: 1rem;
  margin: 0 0 4px;
}

/* ---- 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: var(--muted);
  --placement-active: var(--ink);
}

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

.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: var(--veda-color-surface-subtle);
}

/* 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: 0.875rem;
  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: 0.875rem;
  font-style: italic;
}

/* 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;
  border-radius: 0;
  font-size: 0.875rem;
  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: var(--veda-font-mono);
  font-size: 0.875rem;
  min-width: 0;
  text-align: center;
  /* A text id wraps at a large text size rather than pushing the page sideways. */
  overflow-wrap: anywhere;
}
.unit-switch {
  min-width: 0;
}

.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: 0.875rem;
  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: 0.875rem;
  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: 0.875rem;
  line-height: 1.6;
}

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

.etext-id {
  font-family: var(--veda-font-mono);
  font-size: 0.875rem;
  color: var(--muted);
}

.etext-verse {
  margin: 4px 0 8px;
  font-size: 0.9375rem;
  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: 0.875rem;
  line-height: 1.6;
  font-style: italic;
  color: var(--muted);
}

.etext-source,
.etext-rights,
.etext-missing {
  font-size: 0.875rem;
  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: 0.875rem;
  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: 0.875rem;
  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: 0.875rem;
  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: 0.875rem;
  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: 0.875rem;
  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: var(--veda-radius-sm);
  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: 0.875rem;
  font-weight: 600;
  color: var(--verify-refuted);
}

.unit-absence-reason,
.unit-absence-ids {
  display: block;
  margin-top: 3px;
  font-size: 0.875rem;
  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: var(--veda-font-mono);
  font-size: 0.875rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* 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: var(--veda-radius-sm);
  border: 1px solid var(--verify-refuted);
  color: var(--verify-refuted);
  font-size: 0.875rem;
  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: 0.875rem;
  text-align: center;
}

.unit-row[data-shared] .unit-runs {
  background: var(--accent);
  color: var(--veda-color-on-action);
}

/* 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: 0.875rem;
  text-align: left;
  cursor: pointer;
}

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

.part-range {
  font-family: var(--veda-font-mono);
  font-weight: 700;
}

.part-note {
  flex: 0 1 auto;
  color: var(--muted);
  font-size: 0.875rem;
  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 {
  min-width: 0;
}

/* The column's identity is the EDITION (issue #155), named by its short name
   (display-names.js); which run of it is open is the line below. */
.column-edition-name {
  font-size: 0.875rem;
  font-weight: 700;
}

.column-range {
  font-size: 0.875rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

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

/* The catalog label, quoted whole behind a "Source" disclosure (quiet UI): not
   the column's name, never cut, and bounded so it scrolls when open. */
.column-source {
  flex: 1 0 100%;
  font-size: 0.875rem;
  color: var(--muted);
}
.column-source > summary {
  inline-size: fit-content;
  min-block-size: 1.75rem;
  display: flex;
  align-items: center;
  cursor: pointer;
}
.column-label {
  max-height: 9em;
  overflow-y: auto;
  margin-top: 2px;
  line-height: 1.55;
}
.column-ids {
  margin-top: 2px;
  overflow-wrap: anywhere;
}
/* 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: 0.875rem;
  color: var(--muted);
}

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

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

.column-page {
  font-size: 0.875rem;
  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: 0.875rem;
  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: var(--veda-radius-sm);
  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: 0.875rem;
  font-weight: 700;
  line-height: 1.6;
  color: var(--ink);
}

.column-state-detail {
  margin-top: 4px;
  font-size: 0.875rem;
  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) {
  .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;
  }
}

/* Shared type and control contracts; image widgets retain their own metrics. */
[lang="sa-Latn"] {
  font-family: var(--veda-font-translit);
}
:where(button, select, input, textarea) {
  color: var(--ink);
}
button, select, input, textarea {
  border-color: var(--veda-color-border);
}
.veda-root :focus-visible {
  outline: 3px solid var(--veda-color-focus);
  outline-offset: 2px;
}

/* Compact shared banner; main owns the remaining full-height reading budget. */
.veda-header {
  flex: 0 0 auto;
}
.veda-header__inner {
  padding: var(--veda-space-2) 14px;
  gap: 4px 12px;
}
.veda-brand {
  min-width: 0;
}
body:is(.viewer,.compare) > #main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
}
body.index {
  padding: 0;
  max-width: none;
}
.veda-embed {
  border: 0;
  border-radius: 0;
  min-block-size: 0;
  overflow: hidden;
  background: #20201c;
}
@media (max-width: 700px) {
  .veda-header__inner {
    padding-inline: 10px;
  }
  .veda-brand {
    flex: 1 1 100%;
  }
}

/* Notices keep their DOM and precedence. Canvas/compare/run-seam/catalog/local
   notices are shared `.veda-status` parts; the placeholders below are
   pane-filling specialist surfaces that follow the theme tokens. */
.status-msg, .image-fallback {
  color: var(--veda-color-info); background: var(--veda-color-info-bg);
  border-inline-start: 4px solid var(--veda-color-info);
}
.image-fallback code {
  background: var(--veda-color-surface-subtle);
  overflow-wrap: anywhere;
}
dialog.rights-detail {
  background: var(--panel-bg);
}
.column-note.error {
  border-color: var(--veda-color-danger);
  background: var(--veda-color-danger-bg);
  color: var(--veda-color-danger);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-veda-theme="light"]) {
    /* !important beats model-supplied light palette on the root in dark mode. */
    --status-matched: #86d39f !important;
    --status-partial: #efc262 !important;
    --status-ambiguous: #cdb6f3 !important;
    --status-unmatched: #b9bdc6 !important;
    --verify-refuted: #ff9d92;
    --selected-bg: #40351d;
    --preview-bg: #243348;
    --overlay-ink: 45%;
    --overlay-brightness: 0.55;
  }
}
:root[data-veda-theme="dark"] {
  /* !important beats model-supplied light palette on the root in dark mode. */
  --status-matched: #86d39f !important;
  --status-partial: #efc262 !important;
  --status-ambiguous: #cdb6f3 !important;
  --status-unmatched: #b9bdc6 !important;
  --verify-refuted: #ff9d92;
  --selected-bg: #40351d;
  --preview-bg: #243348;
  --overlay-ink: 45%;
  --overlay-brightness: 0.55;
}

@media (forced-colors: active) {
  .veda-root :is(button, select, .text-card, .unit-row, .mode-switch, .rights-bar) {
    border-color: CanvasText;
  }
  .veda-root :is(.selected, [aria-current], [aria-selected="true"]) {
    outline: 2px solid Highlight;
  }
}

/* Text floor also covers inline code scaled by upstream typography. */
.veda-root code {
  font-size: 0.875rem;
}
/* H1 remains in the main landmark at every width. Let the toolbar wrap. */
body:is(.viewer,.compare) header.app-bar {
  height: auto;
  flex-wrap: wrap;
  gap: 4px 12px;
  padding-block: 4px;
  line-height: 1.4;
}
html[lang] body header.app-bar h1 {
  display: block;
  font-size: 1rem;
}
@media (max-width: 1024px) {
  html[lang] body header.app-bar h1 {
    flex-basis: 100%;
  }
  /* The viewer's short heading shares its row with the edition's name, so the
     name costs no row of the reading area on a phone. */
  html[lang] body.viewer header.app-bar h1 {
    flex-basis: auto;
  }
  body.viewer header.app-bar .edition {
    flex: 1 1 60%;
  }
}
@media (max-width: 400px) {
  body.viewer header.app-bar {
    gap: 4px;
    padding-inline: 8px;
  }
  .canvas-switch {
    gap: 2px;
  }
  .run-switch {
    gap: 2px;
  }
}

/* Preserve independent scrolling and a readable split on narrow screens. */
.split, .compare-main {
  min-width: 0;
}
@media (max-width: 700px) {
  #image-pane {
    min-width: 0;
  }
  #text-pane {
    min-width: 0;
  }
}
/* Keep each edition's reading canvas at its established width on phones. */
@media (max-width: 900px) {
  .edition-columns {
    flex-direction: row;
    overflow-x: auto;
    flex: 1 0 300px;
  }
  .edition-column {
    min-width: 300px;
    flex: 1 0 300px;
  }
  .edition-column:not([data-state="placed"]) {
    min-width: 190px;
    flex: 0 0 230px;
  }
}
/* Grid labels grow with the type floor and fit "Śrautasūtra" spelt out in full.
   Prose wraps without clipping. */
body.index .veda-grid .corner,
body.index .veda-grid tbody th[scope="row"],
body.index .veda-grid tfoot th[scope="row"] {
  width: 112px;
}
@media (max-width: 700px) {
  body.index .veda-grid .corner,
  body.index .veda-grid tbody th[scope="row"],
  body.index .veda-grid tfoot th[scope="row"] {
    width: auto;
  }
}
/* Keep the reading controls together; shrink the decorative legend first. */
body.viewer .legend {
  justify-content: flex-start;
}
/* One row on a desktop (the reading area starts near the top): the legend has
   no basis of its own, so it takes only the room left and clips (it is
   decorative and aria-hidden) instead of pushing a control to a second row. The
   bar may still wrap when the reader's text size needs it, rather than scroll.
   The edition's short name is never cut (a reader's long file name wraps); the work title, which the tab title
   and the run selector's groups also carry, gives way below 1500px. */
@media (min-width: 1025px) {
  body.viewer header.app-bar .legend {
    flex: 1 1 0;
  }
}
@media (max-width: 1500px) {
  body.viewer header.app-bar .edition .work-title {
    display: none;
  }
}
@media (min-width: 1025px) {
  body.viewer header.app-bar {
    column-gap: 8px;
  }
}
.column-state[data-state="unreadable"] {
  border-color: var(--veda-color-danger);
  background: var(--veda-color-danger-bg);
}

.text-card .meta {
  flex-wrap: wrap;
}
.text-card .tid {
  overflow-wrap: anywhere;
}
@media (max-width: 400px) {
  html[lang="en"] .run-switch select {
    max-width: 110px;
  }
  /* On a phone the arrows give up width (not height) as the reader enlarges the
     text, so the one-line label and both arrows still fit at 200%: 88px minus the
     rem-based 44px token is 44px at 100% text and falls to the 32px floor from
     about 150% (above the 24px WCAG 2.2 minimum). */
  .canvas-switch button {
    min-inline-size: max(32px, calc(88px - var(--veda-target-min)));
    padding-inline: 2px;
  }
}
/* Cancel only the upstream embed minimum; retain the OSD host's own minimum. */
.column-osd.veda-embed {
  min-block-size: 220px;
}
@media (max-width: 900px) {
  .column-osd.veda-embed {
    min-block-size: 65vh;
  }
}

/* ---- Report-a-problem footer link (shared design report.js) ----------- */

footer.rights-bar .report-link {
  flex: 0 0 auto;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  min-height: 32px;
}
footer.rights-bar .veda-project-reference {
  flex: 0 1 auto;
  min-width: 0;
  margin: 0;
}
footer.rights-bar .veda-project-reference > a {
  /* Compact reading footer: keep the 0.875rem floor, drop the 44px height so the
     wrapped English line does not eat the 320px reading area. */
  color: var(--accent);
  min-block-size: 0;
  line-height: 1.4;
}
.report-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* "Search for passages like the selection" (assets/js/search-link.js). Fixed so a
   re-rendered pane never moves it; hidden unless a selection is being considered. */
.selection-search {
  position: fixed;
  right: 1rem;
  bottom: 3.5rem;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  max-width: min(28rem, calc(100vw - 2rem));
  padding: 0.5rem 0.75rem;
  color: var(--ink);
  background: var(--panel-bg);
  border: 1px solid var(--line);
  border-radius: var(--veda-radius-sm);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
}

.selection-search[hidden] {
  display: none;
}

.selection-search-note {
  font-size: 0.8125rem;
  color: var(--muted);
}

/* ---- Local mode: the reader's own PDF + e-text (issue #114) ------------- */
/* `viewer.html?import=local`. `body[data-mode="local"]` is set by viewer.js only
   in that mode; `data-local-phase` is input | busy | view. Before a document is
   ready the two panes, the app bar and the rights footer are not shown at all
   (there is nothing to show in them), and once it is, the run switcher and its
   mode switch are gone: they navigate between PUBLISHED runs. */
body[data-mode="local"] .run-switch,
body[data-mode="local"] .mode-switch {
  display: none;
}
body[data-mode="local"]:not([data-local-phase="view"]) .app-bar,
body[data-mode="local"]:not([data-local-phase="view"]) .split,
body[data-mode="local"]:not([data-local-phase="view"]) .rights-bar:not(:has(> .veda-project-reference)),
body[data-mode="local"]:not([data-local-phase="view"]) .rights-bar > :not(.veda-project-reference) {
  display: none;
}
.local-import,
.local-banner {
  min-width: 0;
}
.local-import[hidden],
.local-banner[hidden],
.local-progress[hidden],
.local-error[hidden] {
  display: none;
}
.local-import {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  width: 100%;
  max-width: 52rem;
  margin: 0 auto;
  padding: 16px 20px 40px;
}
.local-import h2 {
  margin: 0 0 8px;
  font-size: 1.25rem;
}
.local-import .note {
  margin: 6px 0;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.6;
}
/* Spacing between the shared form parts; their look is the design system's.
   The script choice keeps its fieldset/legend (it groups radios) but draws no
   frame inside the input group's frame (quiet UI Q5). */
.local-import .veda-fieldset,
.local-field {
  margin-block: var(--veda-space-3);
}
.local-import .local-script {
  border: 0;
  padding: 0;
  margin-inline: 0;
}
.local-import .local-details p {
  margin: 0 0 var(--veda-space-2);
  font-size: 0.875rem;
}

.local-range {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
}
.local-range input[type="number"] {
  width: 6rem;
}
.local-range .veda-hint {
  flex-basis: 100%;
}
.local-actions {
  margin-block: var(--veda-space-3);
}
.local-progress progress {
  width: 100%;
  height: 14px;
}
.local-status {
  min-height: 1.5em;
  margin: 8px 0;
}
.local-error {
  margin-block: var(--veda-space-3);
}
.local-banner {
  flex: 0 0 auto;
  border-radius: 0;
}
.local-banner-main {
  font-weight: 600;
}
.local-banner .local-banner-text {
  margin: 0;
}
.local-banner .local-summary:empty {
  display: none;
}
@media (max-width: 700px) {
  .local-import {
    padding: 12px 12px 32px;
  }
  .local-banner {
    max-height: 40vh;
    overflow: auto;
  }
}
/* The index page's door into local mode. */
.local-entry {
  margin-block: var(--veda-space-4) var(--veda-space-5);
}
.local-entry .local-entry-link {
  align-self: flex-start;
}
/* Once a document is open the input form steps aside (its values stay in the
   DOM, so closing the document returns to them cleared, not rebuilt). */
body[data-mode="local"][data-local-phase="view"] .local-import {
  display: none;
}

/* ---- Shared display settings, pane switch, jump (veda-design 9d65cf4) ---- */

/* Narrow screens (the 700px rung used throughout this file): one pane at a time.
   The panes share one grid cell, so the hidden one keeps its size (deep zoom and
   the text scroll position are not disturbed) and is only invisible and out of
   the tab order. The splitter has nothing to separate and is not shown. */
.pane-switch {
  display: none;
}
@media (max-width: 700px) {
  .pane-switch {
    display: flex;
    flex: 0 0 auto;
    gap: 8px;
    padding: 4px 8px;
    background: var(--veda-color-surface);
    border-block-end: 1px solid var(--line);
  }
  .pane-switch button {
    flex: 1 1 0;
    /* Three buttons where a translation is offered (issue #157): one line each,
       even at 320px, so the switch stays one 44px row. */
    min-width: 0;
    padding-inline: 4px;
    white-space: nowrap;
  }
  .split {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }
  .split > #image-pane,
  .split > #text-pane,
  .split > #translation-pane {
    grid-area: 1 / 1;
    min-width: 0;
    min-height: 0;
  }
  .split > .splitter {
    display: none;
  }
  .split[data-pane="image"] > #text-pane,
  .split[data-pane="text"] > #image-pane,
  .split:not([data-pane="translation"]) > #translation-pane,
  .split[data-pane="translation"] > #image-pane,
  .split[data-pane="translation"] > #text-pane {
    visibility: hidden;
  }
  #translation-pane {
    border-inline-start: none;
  }
  /* The notice (top) and the run seam (foot) are laid out IN the image pane's flow
     here instead of floating over the picture: a phone's pane is short, and two
     absolutely placed bars of three or four lines each covered one another (the
     notice kept only its last line, #282). The picture takes what they leave. */
  #image-pane {
    display: flex;
    flex-direction: column;
  }
  #image-pane > #osd {
    position: relative !important;
    inset: auto;
    flex: 1 1 0;
    min-height: 0;
    order: 1;
  }
  #image-pane > .canvas-notice,
  #image-pane > .run-seam {
    position: static;
    flex: 0 0 auto;
  }
  #image-pane > .canvas-notice {
    order: 0;
  }
  #image-pane > .canvas-notice,
  #image-pane > .run-seam {
    line-height: 1.35;
    padding-block: 3px;
  }
  #image-pane > .run-seam {
    order: 2;
  }
  /* The two bars can take more than a short phone pane has (320px, English: 89px
     + 71px against a ~109px pane, #282 review). While either is shown the picture
     keeps a usable height of its own and the page scrolls (the footer follows the
     reading area) instead of the pane spilling over the footer. */
  .split:has(#image-pane > :is(.canvas-notice, .run-seam):not([hidden])) {
    flex: 1 0 auto;
    min-height: auto;
    grid-template-rows: minmax(auto, 1fr);
  }
  .split:has(#image-pane > :is(.canvas-notice, .run-seam):not([hidden])) > #image-pane {
    min-height: min-content;
  }
  /* Only the image pane sets the floor; the text and translation panes scroll
     inside their cell. */
  .split:has(#image-pane > :is(.canvas-notice, .run-seam):not([hidden])) > :is(#text-pane, #translation-pane) {
    contain: size;
  }
  .split:has(#image-pane > :is(.canvas-notice, .run-seam):not([hidden])) > #image-pane > #osd {
    min-height: 7rem;
    contain: size;
  }
  body.viewer:has(#image-pane > :is(.canvas-notice, .run-seam):not([hidden])) {
    overflow-y: auto;
  }
  body.viewer:has(#image-pane > :is(.canvas-notice, .run-seam):not([hidden])) > #main {
    flex: 1 0 auto;
    min-height: auto;
  }
}
body[data-mode="local"]:not([data-local-phase="view"]) .pane-switch {
  display: none;
}

/* Jump to a work or edition (index.html): shared form parts; only spacing here. */
.work-jump {
  margin-block: var(--veda-space-3);
}
.work-heading:focus-visible,
.edition-heading:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The 44px settings button sets the header's height; keep the rest of the chrome
   compact so the reading area still starts near the top on a desktop. */
.veda-header__inner {
  padding-block: 4px;
}
body:is(.viewer, .compare) header.app-bar {
  min-height: 44px;
}
@media (max-width: 700px) {
  /* Brand and the settings button share the first row, even at 320px. */
  .veda-brand {
    flex: 1 1 6rem;
  }
}
@media (max-width: 700px) {
  /* Every pixel of the 320px column counts: the reading area has to stay usable
     with the app bar, the pane switch and the footer all in view. */
  .pane-switch {
    padding-block: 2px;
  }
  /* The project link is the longest label; it takes its row and breaks evenly,
     so no single character is left alone on a second line. */
  footer.rights-bar .veda-project-reference {
    flex: 1 1 100%;
    text-wrap: balance;
  }
  footer.rights-bar .veda-project-reference > a {
    line-height: 1.3;
  }
}
body:is(.viewer, .compare) header.app-bar {
  /* The 44px shared controls set the bar's height; no extra block padding. */
  padding-block: 0;
}

/* VEDA AI parent link (deploy slot): at narrow widths the brand takes its own
   row and the 44px parent link shares the next one with the settings button,
   so the extra header content costs one row, not a taller brand column. */
.veda-parent-link {
  min-width: 0;
}
@media (max-width: 700px) {
  .veda-header__inner:has(> .veda-parent-link) .veda-brand {
    flex: 1 1 100%;
  }
  .veda-header__inner:has(> .veda-parent-link) .veda-parent-link {
    flex: 1 1 8rem;
  }
  .veda-header__inner:has(> .veda-parent-link) .veda-header__tools {
    margin-inline-start: auto;
  }
}
