/* ============================================================
   The Road to Emmaus — Visualizer
   A constellation chart of the sixty-six books: bars sized by
   verse count, threads drawn as lines between the passages they
   touch, each point placed where it falls within its book.

   Standalone stylesheet: the theme variables are mirrored from
   emmaus.css (change both together) so this page does not inherit
   the timeline's layout rules.
   ============================================================ */

:root {
  --em-ground: #0b0906;
  --em-ground-2: #131009;
  --em-list-bg: #0e0b07;
  --em-bone: #efe8d7;
  --em-bone-dim: #b3a88f;
  --em-dim: #8a7d61;
  --em-faint: #847d6f;
  --em-rule: #332b1a;
  --em-rule-soft: #221c10;
  --em-lens: #ffce8a;
  --em-lens-soft: #f2a65a;
  --em-lens-rgb: 255 206 138;
  --em-tint: 255 255 255;
  --em-shadow-rgb: 0 0 0;
  --em-shadow-a: 0.55;
  --em-raised: 24 20 12;
  --em-scrim: 5 4 2;
  --em-serif: "Newsreader", Georgia, serif;
  --em-sans: "Inter", -apple-system, "Segoe UI", sans-serif;
  --em-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", monospace;

  /* Genre tints. Mixed to sit a long way below the threads in saturation —
     they are meant to be felt as a family of surfaces, not read as a second
     colour code competing with the constellations. Tuned for the near-black
     ground; the light theme has its own, deeper set. */
  --g-narrative: 128 146 176;
  --g-law: 186 154 96;
  --g-poetry: 158 132 190;
  --g-wisdom: 110 168 158;
  --g-lament: 118 126 150;
  --g-prophecy: 190 130 118;
  --g-apocalyptic: 138 120 190;
  --g-gospel: 214 176 110;
  --g-epistle: 132 170 130;
  --g-fill: 0.2;
  --g-stroke: 0.3;
}

:root[data-theme="light"] {
  --em-ground: #faf7f0;
  --em-ground-2: #f1ebdc;
  --em-list-bg: #f5f1e7;
  --em-bone: #241f16;
  --em-bone-dim: #4d4535;
  --em-dim: #5a5342;
  --em-faint: #696356;
  --em-rule: #e2dac6;
  --em-rule-soft: #ece5d5;
  --em-lens: #815c16;
  --em-lens-soft: #85631b;
  --em-lens-rgb: 169 126 43;
  --em-tint: 36 31 22;
  --em-shadow-rgb: 92 74 42;
  --em-shadow-a: 0.14;
  --em-raised: 255 253 247;
  --em-scrim: 60 52 36;

  /* Deeper on cream: the same hues, pushed down in lightness so a bar still
     registers as a shape against the page rather than dissolving into it. */
  --g-narrative: 92 112 145;
  --g-law: 146 116 58;
  --g-poetry: 118 94 152;
  --g-wisdom: 62 124 116;
  --g-lament: 96 104 128;
  --g-prophecy: 150 92 78;
  --g-apocalyptic: 100 84 150;
  --g-gospel: 158 122 58;
  --g-epistle: 88 128 88;
  --g-fill: 0.22;
  --g-stroke: 0.38;
}

html.vz-page,
.vz-page body {
  margin: 0;
  padding: 0;
  background: var(--em-ground);
}

.vz-app {
  min-height: 100dvh;
  background:
    radial-gradient(80rem 26rem at 50% -14rem, rgb(var(--em-lens-rgb) / 0.05) 0%, transparent 65%),
    var(--em-ground);
  color: var(--em-bone);
  font-family: var(--em-sans);
  -webkit-font-smoothing: antialiased;
}

.vz-app ::selection {
  background: rgb(var(--em-lens-rgb) / 0.25);
}

.vz-app :focus-visible,
.vz-dialog :focus-visible {
  outline: 2px solid var(--em-lens);
  outline-offset: 2px;
}

button {
  font: inherit;
  cursor: pointer;
}

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

.vz-top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--em-rule);
}

.vz-titles {
  flex: 1 1 auto;
  min-width: 0;
}

/* The way back, set in the same small caps the rest of the site uses for
   navigation. A filled pill shouted at the top of a page whose whole point is
   the picture below it. */
.vz-back {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 0;
  font-family: var(--em-mono);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--em-dim);
  text-decoration: none;
  transition: color 0.2s ease;
}

.vz-back:hover {
  color: var(--em-lens);
}

.vz-back svg {
  width: 11px;
  height: 11px;
}

.vz-title {
  margin: 1px 0 0;
  font-family: var(--em-serif);
  font-size: 20px;
  font-weight: 500;
  line-height: 1.2;
  color: var(--em-bone);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.vz-title em {
  font-style: italic;
  color: var(--em-lens);
}

.vz-mode {
  margin-left: 8px;
  font-family: var(--em-mono);
  font-size: 9.5px;
  font-style: normal;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--em-dim);
  vertical-align: 3px;
}

.vz-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  margin-left: auto;
}

.vz-iconbtn {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--em-rule);
  background: transparent;
  color: var(--em-bone-dim);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.vz-iconbtn:hover {
  color: var(--em-bone);
  border-color: var(--em-dim);
}

.vz-iconbtn svg {
  width: 18px;
  height: 18px;
}

/* Theme toggle shows the icon of the theme you would switch to. */
.vz-iconbtn .icon-sun { display: none; }
.vz-iconbtn .icon-moon { display: block; }
:root[data-theme="dark"] .vz-iconbtn .icon-sun { display: block; }
:root[data-theme="dark"] .vz-iconbtn .icon-moon { display: none; }

/* ---------- The menu ---------- */

.vz-menu-wrap {
  position: relative;
  flex-shrink: 0;
}

.vz-menu-btn {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--em-rule);
  background: transparent;
  color: var(--em-bone-dim);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.vz-menu-btn svg {
  width: 19px;
  height: 19px;
}

.vz-menu-btn:hover,
.vz-menu-btn[aria-expanded="true"] {
  color: var(--em-bone);
  border-color: var(--em-dim);
}

.vz-menu {
  position: absolute;
  z-index: 70;
  top: calc(100% + 8px);
  right: 0;
  width: max-content;
  min-width: 216px;
  padding: 6px;
  border-radius: 14px;
  border: 1px solid var(--em-rule);
  background: rgb(var(--em-raised) / 0.98);
  box-shadow: 0 20px 56px rgb(var(--em-shadow-rgb) / var(--em-shadow-a));
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.vz-menu[hidden] {
  display: none;
}

.vz-menu-item {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 9px 11px;
  min-height: 44px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--em-bone-dim);
  font-family: var(--em-sans);
  font-size: 13.5px;
  text-align: left;
  text-decoration: none;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.vz-menu-item svg {
  flex-shrink: 0;
  width: 17px;
  height: 17px;
  color: var(--em-dim);
}

.vz-menu-item:hover {
  background: rgb(var(--em-tint) / 0.05);
  color: var(--em-bone);
}

.vz-menu-rule {
  height: 1px;
  margin: 5px 11px;
  background: var(--em-rule);
}

/* Leaving is not one of the chart's own actions. */
.vz-menu-item.is-leave svg {
  color: var(--em-lens-soft);
}

@media (max-width: 639.98px) {
  /* The menu carries the order toggle on a phone, which keeps the control
     bar to a single rank. Scoped to the bar so it outranks the shared button
     rule further down the file rather than depending on source order. */
  .vz-chipbar .vz-order {
    display: none;
  }
}

/* ---------- Control bar ---------- */

.vz-chipbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  background: color-mix(in srgb, var(--em-ground) 86%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--em-rule-soft);
}

/* Fifteen chips were a two-deep wall of words above a picture. One control
   that opens a list holds the same choices in a fifth of the room. */
.vz-filter {
  position: relative;
  flex-shrink: 0;
}

.vz-filter-btn,
.vz-order,
.vz-chipactions button {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  min-height: 34px;
  border-radius: 999px;
  border: 1px solid var(--em-rule);
  background: transparent;
  color: var(--em-bone-dim);
  font-family: var(--em-sans);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.vz-filter-btn:hover,
.vz-order:hover,
.vz-chipactions button:hover {
  color: var(--em-bone);
  border-color: var(--em-dim);
}

/* The axis the chart is drawn against. It names the order in effect, and the
   tint is the state cue when the label is the only thing that changed. */
.vz-order svg {
  width: 14px;
  height: 14px;
  opacity: 0.75;
}

.vz-order.is-alt {
  border-color: color-mix(in srgb, var(--em-lens) 45%, var(--em-rule));
  color: var(--em-lens);
}

.vz-order.is-alt svg {
  opacity: 1;
}

.vz-filter-btn svg {
  width: 15px;
  height: 15px;
}

.vz-filter-chev {
  width: 13px !important;
  height: 13px !important;
  opacity: 0.7;
  transition: transform 0.2s ease;
}

.vz-filter-btn[aria-expanded="true"] {
  color: var(--em-bone);
  border-color: var(--em-dim);
}

.vz-filter-btn[aria-expanded="true"] .vz-filter-chev {
  transform: rotate(180deg);
}

/* How many are lit, on the control itself — the menu is closed most of the
   time and this is the only place that count would otherwise live. */
.vz-filter-count {
  display: none;
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgb(var(--em-lens-rgb) / 0.18);
  color: var(--em-lens);
  font-family: var(--em-mono);
  font-size: 10px;
  text-align: center;
}

.vz-filter-count.is-shown {
  display: inline-block;
}

.vz-filter-menu {
  position: absolute;
  z-index: 60;
  top: calc(100% + 7px);
  left: 0;
  width: min(310px, calc(100vw - 32px));
  max-height: min(58vh, 460px);
  overflow-y: auto;
  padding: 6px;
  border-radius: 14px;
  border: 1px solid var(--em-rule);
  background: rgb(var(--em-raised) / 0.98);
  box-shadow: 0 20px 56px rgb(var(--em-shadow-rgb) / var(--em-shadow-a));
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.vz-filter-menu[hidden] {
  display: none;
}

.vz-filter-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.vz-filter-list button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  min-height: 44px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--em-bone-dim);
  font-family: var(--em-sans);
  text-align: left;
  transition: background-color 0.12s ease;
}

.vz-filter-list button:hover {
  background: rgb(var(--em-tint) / 0.05);
}

.vz-filter-list .dot {
  flex-shrink: 0;
  width: 9px;
  height: 9px;
  border-radius: 99px;
  background: var(--chip);
  box-shadow: 0 0 6px color-mix(in srgb, var(--chip) 60%, transparent);
}

.vz-fi-copy {
  flex: 1;
  min-width: 0;
}

.vz-fi-name {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--em-bone);
}

.vz-fi-sub {
  display: block;
  margin-top: 1px;
  font-size: 11px;
  line-height: 1.35;
  color: var(--em-faint);
}

.vz-fi-check {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  opacity: 0;
  color: var(--chip);
}

.vz-filter-list button[aria-pressed="true"] {
  background: color-mix(in srgb, var(--chip) 12%, transparent);
}

.vz-filter-list button[aria-pressed="true"] .vz-fi-name {
  color: var(--chip);
}

.vz-filter-list button[aria-pressed="true"] .vz-fi-check {
  opacity: 1;
}

.vz-chipactions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.vz-count {
  flex-shrink: 0;
  margin-left: auto;
  font-family: var(--em-mono);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  color: var(--em-faint);
  white-space: nowrap;
}

@media (max-width: 559.98px) {
  .vz-count { display: none; }
}

/* ---------- Stage ---------- */

.vz-stage {
  position: relative;
  margin: 6px auto 0;
  max-width: 1400px;
}

.vz-canvas svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Bars: one flat tint per genre, no texture. Hatching and dashes made the
   chart busy exactly where it needed to be calm — the stars have to be the
   thing the eye lands on, and they cannot win against a field of stripes.
   Each bar is a clean rectangle in a colour so muted it reads as a shade. */
.vz-bar {
  fill: rgb(var(--g) / var(--g-fill));
  stroke: rgb(var(--g) / var(--g-stroke));
  stroke-width: 1;
  opacity: 0.72;
  transition: opacity 0.4s ease;
}

.vz-bar.g-narrative { --g: var(--g-narrative); }
.vz-bar.g-law { --g: var(--g-law); }
.vz-bar.g-poetry { --g: var(--g-poetry); }
.vz-bar.g-wisdom { --g: var(--g-wisdom); }
.vz-bar.g-lament { --g: var(--g-lament); }
.vz-bar.g-prophecy { --g: var(--g-prophecy); }
.vz-bar.g-apocalyptic { --g: var(--g-apocalyptic); }
.vz-bar.g-gospel { --g: var(--g-gospel); }
.vz-bar.g-epistle { --g: var(--g-epistle); }

/* A book the active threads touch comes forward — by coming up to full
   strength rather than by changing colour, so the genus stays legible. */
.vz-bar.is-on { opacity: 1; }

/* The bar under the pointer, named by the tip beside it. */
.vz-bar.is-hover {
  opacity: 1;
  stroke: rgb(var(--em-tint) / 0.55);
}

/* ---------- Book tip (hover, wide screens) ----------
   The axis has room for three-letter stubs only. The full name arrives on
   hover instead of costing the chart a band of vertical space. */
.vz-booktip {
  position: absolute;
  z-index: 35;
  padding: 7px 11px;
  border-radius: 9px;
  border: 1px solid var(--em-rule);
  background: rgb(var(--em-raised) / 0.97);
  box-shadow: 0 10px 28px rgb(var(--em-shadow-rgb) / var(--em-shadow-a));
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  pointer-events: none;
  white-space: nowrap;
}

.vz-booktip[hidden] {
  display: none;
}

.vz-booktip b {
  display: block;
  font-family: var(--em-serif);
  font-size: 14px;
  font-weight: 500;
  color: var(--em-bone);
}

.vz-booktip span {
  display: block;
  margin-top: 2px;
  font-family: var(--em-mono);
  font-size: 9.5px;
  letter-spacing: 0.06em;
  color: var(--em-dim);
}

/* Book labels (horizontal mode: rotated under the baseline) */
.vz-blabel {
  font-family: var(--em-mono);
  font-size: 8px;
  letter-spacing: 0.08em;
  fill: rgb(var(--em-tint) / 0.34);
  transition: fill 0.3s ease;
}

.vz-blabel.is-on { fill: var(--em-bone-dim); }

/* Book names (vertical mode: the rail on the left) */
.vz-bname {
  font-family: var(--em-sans);
  font-size: 11px;
  fill: var(--em-dim);
  transition: fill 0.3s ease;
}

.vz-bname.is-on {
  fill: var(--em-bone);
  font-weight: 500;
}

/* Band labels + baseline. These head the chart now rather than trailing under
   it, so they carry a readable ink rather than a wash — an era should be
   legible without being hunted for. */
.vz-era {
  font-family: var(--em-mono);
  font-size: 8.5px;
  letter-spacing: 0.15em;
  fill: var(--em-dim);
}

/* The seam between one era and the next. Deliberately at the threshold of
   visible: enough to segment the field, not enough to draw the eye off the
   constellations. */
.vz-bandline {
  stroke: rgb(var(--em-tint) / 0.055);
  stroke-width: 1;
}

.vz-era-years {
  font-family: var(--em-mono);
  font-size: 8px;
  letter-spacing: 0.06em;
  fill: rgb(var(--em-tint) / 0.2);
}

/* Under the band header, spanning the chart's field — it is what makes the
   header read as the start of a section rather than as another book label. */
.vz-era-rule {
  stroke: rgb(var(--em-tint) / 0.12);
  stroke-width: 1;
}

.vz-baseline {
  stroke: rgb(var(--em-tint) / 0.18);
  stroke-width: 1;
}


/* The four hundred years */
.vz-silence-rule {
  stroke: rgb(var(--em-tint) / 0.16);
  stroke-width: 1;
  stroke-dasharray: 2 5;
}

.vz-silence-label {
  font-family: var(--em-serif);
  font-style: italic;
  font-size: 10.5px;
  fill: rgb(var(--em-tint) / 0.3);
}

/* Thread lines */
.vz-thread {
  fill: none;
  stroke-width: 1.4;
  stroke-linejoin: round;
  stroke-linecap: round;
  opacity: 0.8;
}

/* ---------- Points ---------- */

.vz-points {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.vz-point {
  position: absolute;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  pointer-events: auto;
  opacity: 0;
  transform: scale(0.3);
  transition: opacity 0.45s ease, transform 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.4);
}

.vz-points.is-ready .vz-point {
  opacity: 1;
  transform: scale(1);
}

/* The star itself */
.vz-point::before {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: 999px;
  background: var(--pt);
  box-shadow:
    0 0 7px color-mix(in srgb, var(--pt) 85%, transparent),
    0 0 16px color-mix(in srgb, var(--pt) 45%, transparent);
}

.vz-point:hover::before,
.vz-point.is-active::before {
  box-shadow:
    0 0 9px var(--pt),
    0 0 26px color-mix(in srgb, var(--pt) 65%, transparent);
}

/* An arrival: ringed in gold, the same mark the timeline uses. */
.vz-point.is-fulfillment::before {
  inset: 5.5px;
  border: 1.6px solid var(--em-lens);
  background: var(--pt);
  box-shadow:
    0 0 8px color-mix(in srgb, var(--pt) 75%, transparent),
    0 0 18px rgb(var(--em-lens-rgb) / 0.5);
}

/* Awaited (future tense): the ring is broken. */
.vz-point.is-awaited::before {
  border-style: dashed;
}

@media (prefers-reduced-motion: reduce) {
  .vz-point { transition: none; }
  .vz-thread { transition: none !important; }
}

/* ---------- Passage card ---------- */

.vz-card {
  position: absolute;
  z-index: 40;
  width: min(320px, calc(100vw - 32px));
  max-height: min(430px, 62vh);
  overflow-y: auto;
  padding: 14px 16px 15px;
  border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--pt, var(--em-rule)) 38%, var(--em-rule));
  background: rgb(var(--em-raised) / 0.97);
  box-shadow: 0 18px 50px rgb(var(--em-shadow-rgb) / var(--em-shadow-a));
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.vz-card.is-open {
  opacity: 1;
  pointer-events: auto;
}

.vz-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
}

.vz-card-dot {
  width: 9px;
  height: 9px;
  border-radius: 99px;
  background: var(--pt);
  box-shadow: 0 0 6px color-mix(in srgb, var(--pt) 60%, transparent);
}

.vz-card-thread {
  font-family: var(--em-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--pt);
}

.vz-card-star {
  color: var(--em-lens);
  font-size: 12px;
}

.vz-card-close {
  margin-left: auto;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--em-dim);
  font-size: 15px;
  line-height: 1;
}

.vz-card-close:hover {
  color: var(--em-bone);
}

.vz-card-book {
  margin-top: 7px;
  font-family: var(--em-serif);
  font-size: 16px;
  color: var(--em-bone);
}

.vz-card-book a {
  color: var(--em-lens);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--em-lens) 45%, transparent);
}

.vz-card blockquote {
  margin: 10px 0 0;
  padding-left: 12px;
  border-left: 2px solid color-mix(in srgb, var(--pt) 55%, transparent);
  font-family: var(--em-serif);
  font-style: italic;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--em-bone-dim);
}

.vz-card-text {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--em-bone-dim);
}

.vz-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 11px;
}

/* Two ways out of the sky, both carrying the thread with them: up to what the
   promise itself is, or across to what this book is saying. The first is the
   filled one — a star raises the question "what is this thread?" long before
   it raises a question about the book it landed in. */
.vz-card-gos {
  display: grid;
  gap: 7px;
  margin-top: 13px;
}

.vz-card-go {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 12px;
  min-height: 42px;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--pt) 40%, transparent);
  background: color-mix(in srgb, var(--pt) 13%, transparent);
  color: var(--pt);
  font-size: 12.5px;
  font-weight: 500;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.vz-card-go:hover {
  background: color-mix(in srgb, var(--pt) 22%, transparent);
}

/* The secondary of the pair: same shape, none of the fill. */
.vz-card-go.is-quiet {
  border-color: var(--em-rule);
  background: transparent;
  color: var(--em-bone-dim);
}

.vz-card-go.is-quiet:hover {
  border-color: var(--em-dim);
  color: var(--em-bone);
}

.vz-card-go svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

.vz-tag {
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--em-rule);
  font-family: var(--em-mono);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--em-faint);
}

/* Wherever the page is not height-locked — a portrait phone, a phone turned
   landscape, a short desktop window — the page can scroll, so a card placed
   against the chart could sit below the fold. Pinning it to the foot of the
   viewport instead means what you tapped is always readable without scrolling
   for it. The conditions here mirror the lock exactly: change both together. */
@media (max-width: 639.98px), (max-height: 619.98px) {
  .vz-card {
    position: fixed;
    left: 10px !important;
    right: 10px;
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    top: auto !important;
    width: auto;
    max-height: 52vh !important;
  }
}

/* ---------- Empty hint ---------- */

.vz-hint {
  position: absolute;
  left: 50%;
  top: 38%;
  transform: translate(-50%, -50%);
  max-width: 30em;
  padding: 0 20px;
  text-align: center;
  font-family: var(--em-serif);
  font-style: italic;
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--em-dim);
  pointer-events: none;
  transition: opacity 0.4s ease;
}

.vz-app.has-threads .vz-hint {
  opacity: 0;
}

/* ---------- Help dialog ---------- */

.vz-dialog {
  margin: auto;
  width: min(92vw, 560px);
  max-height: min(86dvh, 680px);
  padding: 0;
  border: 1px solid var(--em-rule);
  border-radius: 20px;
  background: var(--em-ground-2);
  color: var(--em-bone);
  font-family: var(--em-sans);
  box-shadow: 0 24px 80px rgb(var(--em-shadow-rgb) / var(--em-shadow-a));
}

.vz-dialog::backdrop {
  background: rgb(var(--em-scrim) / 0.72);
  backdrop-filter: blur(2px);
}

.vz-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--em-rule);
}

.vz-dialog-head h2 {
  margin: 0;
  font-family: var(--em-serif);
  font-size: 19px;
  font-weight: 500;
}

.vz-dialog-close {
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--em-dim);
  font-size: 15px;
}

.vz-dialog-close:hover { color: var(--em-bone); }

.vz-dialog-body {
  padding: 14px 20px 20px;
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--em-bone-dim);
}

.vz-dialog-body p { margin: 0 0 10px; }
.vz-dialog-body b { color: var(--em-bone); font-weight: 600; }

.vz-legend {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px 14px;
  margin-top: 12px;
}

.vz-legend-item {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 12px;
  color: var(--em-bone-dim);
}

.vz-legend-item svg {
  flex-shrink: 0;
  display: block;
}

/* ---------- Footer ---------- */

.vz-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  align-items: center;
  padding: 18px 16px 26px;
  font-family: var(--em-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--em-faint);
}

.vz-foot a {
  color: var(--em-dim);
  text-decoration: none;
  border-bottom: 1px solid var(--em-rule);
  padding-bottom: 2px;
}

.vz-foot a:hover { color: var(--em-lens); }

/* Wide screens: the header breathes a little more. */
@media (min-width: 900px) {
  .vz-top { padding: 11px 24px; }
  .vz-chipbar { padding-inline: 24px; }
  .vz-foot { padding-inline: 24px; }
}

/* The whole chart on one screen. The layout sizes the sky to the room left
   over after the chrome, so nothing here should ever have to scroll — this
   makes that a guarantee rather than an intention. Only applied where there
   is genuinely enough height; a short window scrolls normally instead of
   clipping. */
@media (min-width: 640px) and (min-height: 620px) {
  html.vz-page,
  .vz-page body {
    height: 100%;
    overflow: hidden;
  }

  .vz-app {
    height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  /* `1 1 0`, not `1`: with an auto basis the chart's own height feeds back
     into the box that is supposed to be measuring the room for it, and the
     two chase each other a few pixels smaller on every render. A zero basis
     makes the stage purely the leftover space, so measuring it is stable.
     width is explicit because the auto side-margins that centre the stage
     also switch off the flex stretch, which would leave it zero-wide. */
  .vz-stage {
    flex: 1 1 0;
    min-height: 0;
    width: 100%;
    overflow: hidden;
  }

  .vz-foot {
    margin-top: auto;
    padding-block: 10px 12px;
  }
}

/* Hide rotated book labels when the slots get too tight to read them. */
@media (max-width: 979.98px) {
  .vz-blabel { display: none; }
}

/* Phones: every row of chrome above the sky is a row of constellation the
   reader cannot see, so the header collapses to one line of controls with the
   title beneath it, and the lede stays to two lines. */
@media (max-width: 639.98px) {
  .vz-top {
    padding: 8px 14px;
    gap: 8px;
  }

  .vz-title { font-size: 17px; }

  .vz-mode {
    margin-left: 7px;
    vertical-align: 2px;
  }

  .vz-iconbtn { width: 34px; height: 34px; }

  /* Four controls will not fit one phone-width rank. They wrap rather than
     scroll: a scrolling container would clip the thread menu that drops out
     of it, and a control you have to discover by swiping is worse than a
     second row. */
  .vz-chipbar {
    flex-wrap: wrap;
    gap: 7px;
    padding: 8px 14px;
  }

  .vz-foot { padding: 14px 14px 22px; }
}
